@slashfi/agents-sdk 0.6.0 → 0.8.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.
@@ -20,13 +20,42 @@ import type { AgentDefinition, ToolContext, ToolDefinition } from "../types.js";
20
20
  * Pluggable secret storage backend.
21
21
  * Stores encrypted values, resolves refs.
22
22
  */
23
+ /**
24
+ * Scope for multi-tenant secret isolation.
25
+ * When provided, secrets are partitioned by tenant/instance.
26
+ */
27
+ export interface SecretScope {
28
+ tenantId: string;
29
+ instanceKey?: string;
30
+ }
31
+
23
32
  export interface SecretStore {
24
33
  /** Store a secret. Returns the secret ID (without prefix). */
25
- store(value: string, ownerId: string): Promise<string>;
34
+ store(value: string, ownerId: string, scope?: SecretScope): Promise<string>;
35
+
26
36
  /** Resolve a secret ID to its decrypted value. */
27
- resolve(id: string, ownerId: string): Promise<string | null>;
37
+ resolve(id: string, ownerId: string, scope?: SecretScope): Promise<string | null>;
38
+
28
39
  /** Delete a secret. */
29
- delete(id: string, ownerId: string): Promise<boolean>;
40
+ delete(id: string, ownerId: string, scope?: SecretScope): Promise<boolean>;
41
+
42
+ /**
43
+ * Store multiple secrets in a single operation.
44
+ * Returns an array of secret IDs in the same order as the input values.
45
+ */
46
+ storeBatch?(values: string[], ownerId: string, scope?: SecretScope): Promise<string[]>;
47
+
48
+ /**
49
+ * Associate a secret with an entity (e.g., a provider config, a connection).
50
+ * Enables lookup of secrets by entity rather than by ID.
51
+ */
52
+ associate?(secretId: string, entityType: string, entityId: string, scope?: SecretScope): Promise<void>;
53
+
54
+ /**
55
+ * Resolve secrets associated with an entity.
56
+ * Returns all secret IDs linked to the given entity.
57
+ */
58
+ resolveByEntity?(entityType: string, entityId: string, scope?: SecretScope): Promise<string[]>;
30
59
  }
31
60
 
32
61
  // ============================================
@@ -61,27 +90,53 @@ function randomSecretId(): string {
61
90
 
62
91
  export function createInMemorySecretStore(encryptionKey: string): SecretStore {
63
92
  const secrets = new Map<string, { encrypted: string; ownerId: string }>();
93
+ const associations = new Map<string, string[]>(); // "entityType:entityId" -> secretIds
64
94
 
65
95
  return {
66
- async store(value, ownerId) {
96
+ async store(value, ownerId, _scope?) {
67
97
  const id = randomSecretId();
68
98
  const encrypted = await encryptSecret(value, encryptionKey);
69
99
  secrets.set(id, { encrypted, ownerId });
70
100
  return id;
71
101
  },
72
102
 
73
- async resolve(id, ownerId) {
103
+ async resolve(id, ownerId, _scope?) {
74
104
  const entry = secrets.get(id);
75
105
  if (!entry || entry.ownerId !== ownerId) return null;
76
106
  return decryptSecret(entry.encrypted, encryptionKey);
77
107
  },
78
108
 
79
- async delete(id, ownerId) {
109
+ async delete(id, ownerId, _scope?) {
80
110
  const entry = secrets.get(id);
81
111
  if (!entry || entry.ownerId !== ownerId) return false;
82
112
  secrets.delete(id);
83
113
  return true;
84
114
  },
115
+
116
+ async storeBatch(values, ownerId, _scope?) {
117
+ const ids: string[] = [];
118
+ for (const value of values) {
119
+ const id = randomSecretId();
120
+ const encrypted = await encryptSecret(value, encryptionKey);
121
+ secrets.set(id, { encrypted, ownerId });
122
+ ids.push(id);
123
+ }
124
+ return ids;
125
+ },
126
+
127
+ async associate(secretId, entityType, entityId, _scope?) {
128
+ const key = `${entityType}:${entityId}`;
129
+ const existing = associations.get(key) ?? [];
130
+ if (!existing.includes(secretId)) {
131
+ existing.push(secretId);
132
+ associations.set(key, existing);
133
+ }
134
+ },
135
+
136
+ async resolveByEntity(entityType, entityId, _scope?) {
137
+ const key = `${entityType}:${entityId}`;
138
+ return associations.get(key) ?? [];
139
+ },
85
140
  };
86
141
  }
87
142
 
package/src/index.ts CHANGED
@@ -125,6 +125,7 @@ export {
125
125
  processSecretParams,
126
126
  } from "./agent-definitions/secrets.js";
127
127
  export type {
128
+ SecretScope,
128
129
  SecretStore,
129
130
  SecretsAgentOptions,
130
131
  } from "./agent-definitions/secrets.js";
@@ -157,13 +158,16 @@ export type {
157
158
  IntegrationCallInput,
158
159
  RestCallInput,
159
160
  GraphqlCallInput,
160
- AgentRegistryCallInput,
161
161
  UserConnection,
162
162
  ClientAuthMethod,
163
163
  TokenContentType,
164
164
  TokenExchangeResult,
165
165
  } from "./agent-definitions/integrations.js";
166
166
 
167
+
168
+ // Remote Registry
169
+ export { createRemoteRegistryAgent } from "./agent-definitions/remote-registry.js";
170
+ export type { RemoteRegistryAgentOptions } from "./agent-definitions/remote-registry.js";
167
171
  // Users
168
172
  export {
169
173
  createUsersAgent,
package/src/registry.ts CHANGED
@@ -290,6 +290,12 @@ export function createAgentRegistry(
290
290
  };
291
291
 
292
292
  try {
293
+ if (!tool.execute) {
294
+ return {
295
+ success: false,
296
+ error: `Tool ${request.tool} has no execute function`,
297
+ } as CallAgentErrorResponse;
298
+ }
293
299
  const result = await tool.execute(request.params, ctx);
294
300
  return {
295
301
  success: true,
package/src/server.ts CHANGED
@@ -14,8 +14,10 @@
14
14
  * - list_agents → List registered agents and their tools
15
15
  *
16
16
  * Additional endpoints:
17
- * - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
18
- * - GET /health Health check
17
+ * - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
18
+ * - GET /oauth/callback Unified OAuth callback (provider from state)
19
+ * - GET /integrations/callback/* → Legacy OAuth callback (provider from URL path)
20
+ * - GET /health → Health check
19
21
  *
20
22
  * Auth Integration:
21
23
  * When an `@auth` agent is registered, the server automatically:
@@ -702,13 +704,8 @@ export function createAgentServer(
702
704
  }
703
705
 
704
706
 
705
- // GET /integrations/callback/:provider - OAuth callback
706
- if (path.startsWith("/integrations/callback/") && req.method === "GET") {
707
- const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
708
- if (!provider) {
709
- return addCors(jsonResponse({ error: "Missing provider" }, 400));
710
- }
711
-
707
+ // ---- Shared OAuth callback handler ----
708
+ async function handleIntegrationOAuthCallback(provider: string, req: Request): Promise<Response> {
712
709
  const url = new URL(req.url);
713
710
  const code = url.searchParams.get("code");
714
711
  const state = url.searchParams.get("state");
@@ -726,7 +723,6 @@ export function createAgentServer(
726
723
  return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
727
724
  }
728
725
 
729
- // Call handle_oauth_callback tool on @integrations
730
726
  try {
731
727
  await registry.call({
732
728
  action: "execute_tool",
@@ -741,13 +737,19 @@ export function createAgentServer(
741
737
  },
742
738
  } as any);
743
739
 
744
- // Parse redirect URL from state
740
+ // Parse redirect URL from state (base64-encoded JSON)
745
741
  let redirectUrl = "/";
746
742
  if (state) {
747
743
  try {
748
- const parsed = JSON.parse(state);
744
+ const parsed = JSON.parse(atob(state));
749
745
  if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
750
- } catch {}
746
+ } catch {
747
+ // Fallback: try raw JSON for backward compat
748
+ try {
749
+ const parsed = JSON.parse(state);
750
+ if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
751
+ } catch {}
752
+ }
751
753
  }
752
754
 
753
755
  const sep = redirectUrl.includes("?") ? "&" : "?";
@@ -760,6 +762,38 @@ export function createAgentServer(
760
762
  }
761
763
  }
762
764
 
765
+ // GET /oauth/callback - Unified OAuth callback (provider from state param)
766
+ if (path === "/oauth/callback" && req.method === "GET") {
767
+ const url = new URL(req.url);
768
+ const state = url.searchParams.get("state");
769
+ let provider: string | undefined;
770
+ if (state) {
771
+ try {
772
+ const parsed = JSON.parse(atob(state));
773
+ provider = parsed.providerId;
774
+ } catch {
775
+ // Fallback: try raw JSON for backward compat
776
+ try {
777
+ const parsed = JSON.parse(state);
778
+ provider = parsed.providerId;
779
+ } catch {}
780
+ }
781
+ }
782
+ if (!provider) {
783
+ return addCors(jsonResponse({ error: "Missing provider in state param" }, 400));
784
+ }
785
+ return handleIntegrationOAuthCallback(provider, req);
786
+ }
787
+
788
+ // GET /integrations/callback/:provider - Legacy OAuth callback (provider from URL path)
789
+ if (path.startsWith("/integrations/callback/") && req.method === "GET") {
790
+ const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
791
+ if (!provider) {
792
+ return addCors(jsonResponse({ error: "Missing provider" }, 400));
793
+ }
794
+ return handleIntegrationOAuthCallback(provider, req);
795
+ }
796
+
763
797
 
764
798
  // GET /secrets/form/:token - Serve hosted secrets form
765
799
  if (path.startsWith("/secrets/form/") && req.method === "GET") {
package/src/types.ts CHANGED
@@ -421,8 +421,21 @@ export interface ToolDefinition<
421
421
 
422
422
  /**
423
423
  * Execute the tool with validated input.
424
+ * Optional — some registries load tool implementations dynamically.
424
425
  */
425
- execute: (input: TInput, ctx: TContext) => Promise<TOutput>;
426
+ execute?: (input: TInput, ctx: TContext) => Promise<TOutput>;
427
+
428
+ /**
429
+ * Path to the tool source file (e.g., '/agents/@clock/timer.tool.ts').
430
+ * Used for tool discovery and prompt composition.
431
+ */
432
+ path?: string;
433
+
434
+ /**
435
+ * Full documentation content for system prompt composition.
436
+ * When set, rendered directly into the agent's system prompt.
437
+ */
438
+ doc?: string;
426
439
  }
427
440
 
428
441
  /**
@@ -473,6 +486,12 @@ export interface AgentDefinition<TContext extends ToolContext = ToolContext> {
473
486
  * by @integrations via standard methods (setup, list, connect, get, update).
474
487
  */
475
488
  integrationMethods?: IntegrationMethods;
489
+
490
+ /**
491
+ * Lazy loader for lifecycle listeners.
492
+ * Called once to load runtime hooks exported from the agent's entrypoint module.
493
+ */
494
+ loadListeners?: () => Promise<unknown>;
476
495
  }
477
496
 
478
497
  // ============================================