@slashfi/agents-sdk 0.78.0 → 0.79.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.
@@ -1155,3 +1155,157 @@ describe("ADK ref registry cache", () => {
1155
1155
  expect(refs[0].description).toBeUndefined();
1156
1156
  });
1157
1157
  });
1158
+
1159
+ // ─── isRefAuthComplete + authFields cache ────────────────────────
1160
+
1161
+ describe("isRefAuthComplete + cached authFields", () => {
1162
+ /**
1163
+ * The core idea: `auth-status` knows what fields are required for a
1164
+ * given security scheme (it asks the registry). Cache that answer
1165
+ * shape per-ref so subsequent host-side "is this ref ready to call?"
1166
+ * checks can be evaluated locally with no network round-trip, and
1167
+ * stay accurate as the user fills in or clears credentials in the
1168
+ * entry's config.
1169
+ *
1170
+ * `isRefAuthComplete(entry, cacheEntry)` returns:
1171
+ * - `true` when all required fields are satisfied (present in
1172
+ * `entry.config` OR marked `automated`).
1173
+ * - `false` when at least one required, non-automated field is
1174
+ * missing.
1175
+ * - `null` when the cache has no `authFields` for this ref yet
1176
+ * (caller should fall back or refresh via `auth-status`).
1177
+ */
1178
+
1179
+ test("cache miss returns null", async () => {
1180
+ const { isRefAuthComplete } = await import("./config-store");
1181
+ const result = isRefAuthComplete(
1182
+ {
1183
+ ref: "@unknown",
1184
+ name: "@unknown",
1185
+ scheme: "https",
1186
+ url: "http://localhost",
1187
+ },
1188
+ undefined,
1189
+ );
1190
+ expect(result).toBeNull();
1191
+ });
1192
+
1193
+ test("proxy mode short-circuits to true regardless of cache", async () => {
1194
+ const { isRefAuthComplete } = await import("./config-store");
1195
+ const result = isRefAuthComplete(
1196
+ {
1197
+ ref: "slash",
1198
+ name: "slash",
1199
+ scheme: "registry",
1200
+ // proxy mode set by ref.add when registry inspection includes it.
1201
+ // biome-ignore lint/suspicious/noExplicitAny: mode isn't on the public type
1202
+ mode: "proxy",
1203
+ } as any,
1204
+ undefined,
1205
+ );
1206
+ expect(result).toBe(true);
1207
+ });
1208
+
1209
+ test("required field present → true", async () => {
1210
+ const { isRefAuthComplete } = await import("./config-store");
1211
+ const result = isRefAuthComplete(
1212
+ {
1213
+ ref: "@oauth",
1214
+ name: "@oauth",
1215
+ scheme: "https",
1216
+ url: "http://localhost",
1217
+ config: {
1218
+ client_id: "abc",
1219
+ client_secret: "xyz",
1220
+ access_token: "tok",
1221
+ },
1222
+ },
1223
+ {
1224
+ ref: "@oauth",
1225
+ fetchedAt: new Date().toISOString(),
1226
+ authFields: {
1227
+ client_id: { required: true, automated: false },
1228
+ client_secret: { required: true, automated: false },
1229
+ access_token: { required: true, automated: true },
1230
+ },
1231
+ },
1232
+ );
1233
+ expect(result).toBe(true);
1234
+ });
1235
+
1236
+ test("automated field absent still counts as satisfied", async () => {
1237
+ const { isRefAuthComplete } = await import("./config-store");
1238
+ // dynamicRegistration: client_id is automated, so absence is fine.
1239
+ const result = isRefAuthComplete(
1240
+ {
1241
+ ref: "@oauth",
1242
+ name: "@oauth",
1243
+ scheme: "https",
1244
+ url: "http://localhost",
1245
+ config: {
1246
+ // client_id missing
1247
+ access_token: "tok",
1248
+ },
1249
+ },
1250
+ {
1251
+ ref: "@oauth",
1252
+ fetchedAt: new Date().toISOString(),
1253
+ authFields: {
1254
+ client_id: { required: true, automated: true },
1255
+ access_token: { required: true, automated: true },
1256
+ },
1257
+ },
1258
+ );
1259
+ expect(result).toBe(true);
1260
+ });
1261
+
1262
+ test("required, non-automated field missing → false", async () => {
1263
+ const { isRefAuthComplete } = await import("./config-store");
1264
+ const result = isRefAuthComplete(
1265
+ {
1266
+ ref: "@oauth",
1267
+ name: "@oauth",
1268
+ scheme: "https",
1269
+ url: "http://localhost",
1270
+ config: {
1271
+ client_id: "abc",
1272
+ client_secret: "xyz",
1273
+ // access_token missing — user hasn't completed OAuth yet.
1274
+ },
1275
+ },
1276
+ {
1277
+ ref: "@oauth",
1278
+ fetchedAt: new Date().toISOString(),
1279
+ authFields: {
1280
+ client_id: { required: true, automated: false },
1281
+ client_secret: { required: true, automated: false },
1282
+ access_token: { required: true, automated: false },
1283
+ },
1284
+ },
1285
+ );
1286
+ expect(result).toBe(false);
1287
+ });
1288
+
1289
+ test("non-required field absence is fine", async () => {
1290
+ const { isRefAuthComplete } = await import("./config-store");
1291
+ const result = isRefAuthComplete(
1292
+ {
1293
+ ref: "@oauth",
1294
+ name: "@oauth",
1295
+ scheme: "https",
1296
+ url: "http://localhost",
1297
+ config: { access_token: "tok" },
1298
+ },
1299
+ {
1300
+ ref: "@oauth",
1301
+ fetchedAt: new Date().toISOString(),
1302
+ authFields: {
1303
+ client_id: { required: false, automated: false },
1304
+ access_token: { required: true, automated: false },
1305
+ },
1306
+ },
1307
+ );
1308
+ expect(result).toBe(true);
1309
+ });
1310
+ });
1311
+
@@ -68,16 +68,49 @@ export interface RegistryCacheToolSummary {
68
68
  }
69
69
 
70
70
  /**
71
- * Per-ref cache entry. Updated as a side-effect of `ref.add()` and
72
- * `ref.inspect()` whenever the registry response carries description or tool
73
- * information. Identity-relative (lives next to the consumer-config that
74
- * issued the registry call), so permission-filtered views stay consistent.
71
+ * Slim auth-field metadata cached so hosts can locally answer "is this
72
+ * ref ready to call?" without a registry round-trip. Mirrors the
73
+ * authoritative shape `auth-status` produces same source of truth,
74
+ * just persisted.
75
+ *
76
+ * For each field name in the security scheme:
77
+ * - `required` — must end up satisfied for `ref.call` to work.
78
+ * - `automated` — adk fills this in itself (e.g. dynamic OAuth
79
+ * client registration). Doesn't need to be `present`
80
+ * in the user's config to count as satisfied.
81
+ */
82
+ export interface RegistryCacheAuthField {
83
+ required: boolean;
84
+ automated: boolean;
85
+ }
86
+
87
+ /**
88
+ * Per-ref cache entry. Updated as a side-effect of `ref.add()`,
89
+ * `ref.inspect()`, and `ref.authStatus()` whenever the registry
90
+ * response carries description / tool / security-scheme info.
91
+ * Identity-relative (lives next to the consumer-config that issued
92
+ * the registry call), so permission-filtered views stay consistent.
75
93
  */
76
94
  export interface RegistryCacheEntry {
77
95
  /** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
78
96
  ref: string;
79
97
  description?: string;
80
98
  tools?: RegistryCacheToolSummary[];
99
+ /**
100
+ * Auth field requirements derived from the registry's security
101
+ * scheme (extracted by `auth-status`). When present, hosts can
102
+ * compute "is this ref callable?" by intersecting these with the
103
+ * entry's `config` — no network round-trip needed. Absent when the
104
+ * scheme couldn't be fetched (e.g. registry was offline at add
105
+ * time); fall back to whatever heuristic the caller chooses.
106
+ *
107
+ * Note on proxy refs: when the entry is in `proxy` mode the
108
+ * security scheme is exposed by the *proxy*, and the answer to
109
+ * "is this callable?" lives server-side — `authFields` is omitted
110
+ * locally and hosts should treat proxy refs as authoritative
111
+ * regardless of entry-side fields.
112
+ */
113
+ authFields?: Record<string, RegistryCacheAuthField>;
81
114
  /** ISO timestamp of the most recent registry round-trip that wrote this. */
82
115
  fetchedAt: string;
83
116
  }
@@ -91,6 +124,46 @@ export interface RegistryCache {
91
124
  refs: Record<string, RegistryCacheEntry>;
92
125
  }
93
126
 
127
+ /**
128
+ * "Is this ref ready to call?" answered locally using the cached
129
+ * security-scheme requirements. Mirrors the `complete` boolean
130
+ * `auth-status` returns, but doesn't need a network round-trip — the
131
+ * cached `authFields` capture what the registry said is required, and
132
+ * we evaluate satisfaction against the entry's current `config`.
133
+ *
134
+ * Behavior:
135
+ * - `mode: 'proxy'` refs → always true. Auth lives server-side; the
136
+ * proxy is the source of truth, no entry-side fields involved.
137
+ * - Cache miss (no `authFields` for this ref yet) → returns `null`,
138
+ * signaling "I don't know — caller should fall back to its own
139
+ * heuristic or call `auth-status` to populate the cache".
140
+ * - Cache hit → for every required, non-automated field, checks
141
+ * presence in `entry.config`. Mirrors the `present || resolvable`
142
+ * check in `auth-status` but evaluates against current config.
143
+ * `automated` fields (e.g. dynamic OAuth client_id) count as
144
+ * satisfied even when absent — adk supplies them at call time.
145
+ *
146
+ * Returning `null` for cache miss is intentional. A boolean would
147
+ * force callers to choose a default that's wrong half the time;
148
+ * `null` lets them branch explicitly.
149
+ */
150
+ export function isRefAuthComplete(
151
+ entry: RefEntry,
152
+ cacheEntry: RegistryCacheEntry | undefined,
153
+ ): boolean | null {
154
+ if (typeof entry === "string") return false;
155
+ if ((entry as { mode?: unknown }).mode === "proxy") return true;
156
+ const authFields = cacheEntry?.authFields;
157
+ if (!authFields) return null;
158
+ const config = entry.config ?? {};
159
+ for (const [field, info] of Object.entries(authFields)) {
160
+ if (!info.required) continue;
161
+ if (info.automated) continue;
162
+ if (!(field in config)) return false;
163
+ }
164
+ return true;
165
+ }
166
+
94
167
  // ============================================
95
168
  // Types
96
169
  // ============================================
@@ -650,6 +723,29 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
650
723
  await writeRegistryCache(cache);
651
724
  }
652
725
 
726
+ /**
727
+ * Merge `authFields` into an existing cache entry without clobbering
728
+ * description/tools, or create a minimal entry if one doesn't exist
729
+ * yet. Called from `authStatus` so the slim {required, automated}
730
+ * shape is always available for `isRefAuthComplete` to answer
731
+ * locally on subsequent calls.
732
+ */
733
+ async function upsertRegistryCacheAuthFields(
734
+ name: string,
735
+ ref: string,
736
+ authFields: Record<string, RegistryCacheAuthField>,
737
+ ): Promise<void> {
738
+ const cache = await readRegistryCache();
739
+ const existing = cache.refs[name];
740
+ cache.refs[name] = {
741
+ ...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
742
+ authFields,
743
+ // Refresh fetchedAt so freshness telemetry stays accurate.
744
+ fetchedAt: new Date().toISOString(),
745
+ };
746
+ await writeRegistryCache(cache);
747
+ }
748
+
653
749
  async function removeRegistryCacheEntry(name: string): Promise<void> {
654
750
  const cache = await readRegistryCache();
655
751
  if (!(name in cache.refs)) return;
@@ -2342,6 +2438,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2342
2438
  (f) => !f.required || f.present || f.resolvable,
2343
2439
  );
2344
2440
 
2441
+ // Persist the slim {required, automated} per-field shape into the
2442
+ // registry cache so `isRefAuthComplete` can answer subsequent
2443
+ // host-side "is this ref ready?" checks without re-fetching the
2444
+ // security scheme. We deliberately omit `present`/`resolvable`
2445
+ // because those are computed against the current entry.config and
2446
+ // host environment — caching them would go stale immediately.
2447
+ const authFields: Record<string, RegistryCacheAuthField> = {};
2448
+ for (const [field, info] of Object.entries(fields)) {
2449
+ authFields[field] = {
2450
+ required: info.required,
2451
+ automated: info.automated,
2452
+ };
2453
+ }
2454
+ await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
2455
+
2345
2456
  return { name, security, complete, fields };
2346
2457
  },
2347
2458
 
package/src/index.ts CHANGED
@@ -437,7 +437,9 @@ export type {
437
437
  RegistryCache,
438
438
  RegistryCacheEntry,
439
439
  RegistryCacheToolSummary,
440
+ RegistryCacheAuthField,
440
441
  } from "./config-store.js";
442
+ export { isRefAuthComplete } from "./config-store.js";
441
443
  export { createLocalFsStore, getLocalEncryptionKey } from "./local-fs.js";
442
444
  export { AdkError, getError, getRecentErrors } from "./adk-error.js";
443
445
  export {