@okrlinkhub/agent-bridge 0.1.0 → 2.0.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.
Files changed (58) hide show
  1. package/README.md +96 -127
  2. package/dist/cli/init.d.ts +3 -0
  3. package/dist/cli/init.d.ts.map +1 -0
  4. package/dist/cli/init.js +100 -0
  5. package/dist/cli/init.js.map +1 -0
  6. package/dist/client/index.d.ts +50 -173
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +129 -263
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/component/_generated/api.d.ts +4 -4
  11. package/dist/component/_generated/api.d.ts.map +1 -1
  12. package/dist/component/_generated/component.d.ts +66 -162
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/agentBridgeUtils.d.ts +8 -0
  15. package/dist/component/agentBridgeUtils.d.ts.map +1 -0
  16. package/dist/component/agentBridgeUtils.js +33 -0
  17. package/dist/component/agentBridgeUtils.js.map +1 -0
  18. package/dist/component/agents.d.ts +27 -0
  19. package/dist/component/agents.d.ts.map +1 -0
  20. package/dist/component/agents.js +94 -0
  21. package/dist/component/agents.js.map +1 -0
  22. package/dist/component/gateway.d.ts +30 -44
  23. package/dist/component/gateway.d.ts.map +1 -1
  24. package/dist/component/gateway.js +127 -132
  25. package/dist/component/gateway.js.map +1 -1
  26. package/dist/component/permissions.d.ts +30 -84
  27. package/dist/component/permissions.d.ts.map +1 -1
  28. package/dist/component/permissions.js +80 -203
  29. package/dist/component/permissions.js.map +1 -1
  30. package/dist/component/schema.d.ts +55 -153
  31. package/dist/component/schema.d.ts.map +1 -1
  32. package/dist/component/schema.js +30 -80
  33. package/dist/component/schema.js.map +1 -1
  34. package/dist/react/index.d.ts +2 -2
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js +2 -3
  37. package/dist/react/index.js.map +1 -1
  38. package/package.json +7 -3
  39. package/src/cli/init.ts +116 -0
  40. package/src/client/index.ts +228 -389
  41. package/src/component/_generated/api.ts +4 -4
  42. package/src/component/_generated/component.ts +79 -195
  43. package/src/component/agentBridgeUtils.ts +52 -0
  44. package/src/component/agents.ts +106 -0
  45. package/src/component/gateway.ts +149 -163
  46. package/src/component/permissions.ts +89 -259
  47. package/src/component/schema.ts +31 -96
  48. package/src/react/index.ts +5 -6
  49. package/dist/component/provisioning.d.ts +0 -87
  50. package/dist/component/provisioning.d.ts.map +0 -1
  51. package/dist/component/provisioning.js +0 -343
  52. package/dist/component/provisioning.js.map +0 -1
  53. package/dist/component/registry.d.ts +0 -46
  54. package/dist/component/registry.d.ts.map +0 -1
  55. package/dist/component/registry.js +0 -121
  56. package/dist/component/registry.js.map +0 -1
  57. package/src/component/provisioning.ts +0 -402
  58. package/src/component/registry.ts +0 -152
@@ -1,59 +1,22 @@
1
1
  import { v } from "convex/values";
2
2
  import { mutation, query } from "./_generated/server.js";
3
-
4
- // --- Token hashing utility (same as provisioning.ts) ---
5
-
6
- async function hashToken(token: string): Promise<string> {
7
- const encoder = new TextEncoder();
8
- const data = encoder.encode(token);
9
- const hash = await crypto.subtle.digest("SHA-256", data);
10
- return Array.from(new Uint8Array(hash))
11
- .map((b) => b.toString(16).padStart(2, "0"))
12
- .join("");
13
- }
14
-
15
- // --- Pattern matching (same logic as permissions.ts) ---
16
-
17
- function patternSpecificity(pattern: string): number {
18
- const wildcardIndex = pattern.indexOf("*");
19
- if (wildcardIndex === -1) return pattern.length;
20
- return wildcardIndex;
21
- }
22
-
23
- function matchesPattern(functionName: string, pattern: string): boolean {
24
- if (pattern === "*") return true;
25
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
26
- const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
27
- return new RegExp(regexStr).test(functionName);
28
- }
29
-
30
- // --- Authorize request result validator ---
3
+ import {
4
+ findBestPermissionMatch,
5
+ hashApiKey,
6
+ type PermissionType,
7
+ } from "./agentBridgeUtils.js";
31
8
 
32
9
  const authorizeResultValidator = v.union(
33
10
  v.object({
34
11
  authorized: v.literal(true),
35
- agentId: v.string(),
36
- appName: v.string(),
37
- functionHandle: v.string(),
38
- functionType: v.union(
39
- v.literal("query"),
40
- v.literal("mutation"),
41
- v.literal("action"),
42
- ),
12
+ agentId: v.id("agents"),
43
13
  }),
44
14
  v.object({
45
15
  authorized: v.literal(false),
46
16
  error: v.string(),
47
17
  statusCode: v.number(),
48
- agentId: v.optional(v.string()),
49
- matchedPattern: v.optional(v.string()),
50
- matchedPermission: v.optional(
51
- v.union(
52
- v.literal("allow"),
53
- v.literal("deny"),
54
- v.literal("rate_limited"),
55
- ),
56
- ),
18
+ agentId: v.optional(v.id("agents")),
19
+ retryAfterSeconds: v.optional(v.number()),
57
20
  }),
58
21
  );
59
22
 
@@ -62,132 +25,121 @@ const authorizeResultValidator = v.union(
62
25
  * This is a mutation (not a query) because it updates counters and last activity.
63
26
  *
64
27
  * Steps:
65
- * 1. Validate instance token
28
+ * 1. Validate API key
66
29
  * 2. Check agent is active
67
30
  * 3. Check function permissions
68
- * 4. Look up function handle in registry
69
- * 5. Update activity counters
31
+ * 4. Check function global override
32
+ * 5. Check rate limits
70
33
  *
71
- * Returns the function handle info if authorized, or an error.
34
+ * Returns the agent id if authorized, or an error.
72
35
  */
73
36
  export const authorizeRequest = mutation({
74
37
  args: {
75
- instanceToken: v.string(),
76
- functionName: v.string(),
77
- appName: v.string(),
38
+ apiKey: v.string(),
39
+ functionKey: v.string(),
40
+ estimatedCost: v.optional(v.number()),
78
41
  },
79
42
  returns: authorizeResultValidator,
80
43
  handler: async (ctx, args) => {
81
- // 1. Validate instance token
82
- const tokenHash = await hashToken(args.instanceToken);
83
- const instance = await ctx.db
84
- .query("agentAppInstances")
85
- .withIndex("by_instance_token_hash", (q) =>
86
- q.eq("instanceTokenHash", tokenHash),
87
- )
44
+ const apiKeyHash = await hashApiKey(args.apiKey);
45
+ const agent = await ctx.db
46
+ .query("agents")
47
+ .withIndex("by_apiKeyHash", (q) => q.eq("apiKeyHash", apiKeyHash))
88
48
  .unique();
89
-
90
- if (!instance) {
49
+ if (!agent) {
91
50
  return {
92
51
  authorized: false as const,
93
- error: "Invalid instance token",
52
+ error: "Invalid API key",
94
53
  statusCode: 401,
95
54
  };
96
55
  }
97
56
 
98
- if (instance.expiresAt < Date.now()) {
57
+ if (!agent.enabled) {
99
58
  return {
100
59
  authorized: false as const,
101
- error: "Instance token has expired",
102
- statusCode: 401,
60
+ error: "Agent disabled",
61
+ statusCode: 403,
62
+ agentId: agent._id,
103
63
  };
104
64
  }
105
65
 
106
- if (instance.appName !== args.appName) {
66
+ const permissions = await ctx.db
67
+ .query("agentPermissions")
68
+ .withIndex("by_agentId", (q) => q.eq("agentId", agent._id))
69
+ .collect();
70
+ const matchedRule = findBestPermissionMatch(args.functionKey, permissions);
71
+ if (!matchedRule || matchedRule.permission === "deny") {
107
72
  return {
108
73
  authorized: false as const,
109
- error: "Token does not match this app",
74
+ error: `Function ${args.functionKey} not allowed`,
110
75
  statusCode: 403,
76
+ agentId: agent._id,
111
77
  };
112
78
  }
113
79
 
114
- const agentId = instance.agentId;
115
-
116
- // 2. Check agent is active
117
- const agent = await ctx.db
118
- .query("registeredAgents")
119
- .withIndex("by_agent_id", (q) => q.eq("agentId", agentId))
80
+ const functionOverride = await ctx.db
81
+ .query("agentFunctions")
82
+ .withIndex("by_key", (q) => q.eq("key", args.functionKey))
120
83
  .unique();
121
-
122
- if (!agent || !agent.isActive) {
84
+ if (functionOverride && !functionOverride.enabled) {
123
85
  return {
124
86
  authorized: false as const,
125
- error: "Agent has been revoked",
87
+ error: `Function ${args.functionKey} disabled`,
126
88
  statusCode: 403,
127
- agentId,
89
+ agentId: agent._id,
128
90
  };
129
91
  }
130
92
 
131
- // 3. Check permissions
132
- const permissions = await ctx.db
133
- .query("functionPermissions")
134
- .withIndex("by_agent_and_app", (q) =>
135
- q.eq("agentId", agentId).eq("appName", args.appName),
136
- )
93
+ const effectiveHourlyLimit = resolveEffectiveHourlyLimit(
94
+ agent.rateLimit,
95
+ matchedRule.permission,
96
+ matchedRule.rateLimitConfig?.requestsPerHour,
97
+ functionOverride?.globalRateLimit,
98
+ );
99
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
100
+ const recentLogs = await ctx.db
101
+ .query("agentLogs")
102
+ .withIndex("by_agentId_and_timestamp", (q) => q.eq("agentId", agent._id))
137
103
  .collect();
138
-
139
- const matches = permissions
140
- .filter((p) => matchesPattern(args.functionName, p.functionPattern))
141
- .sort(
142
- (a, b) =>
143
- patternSpecificity(b.functionPattern) -
144
- patternSpecificity(a.functionPattern),
145
- );
146
-
147
- if (matches.length === 0 || matches[0].permission === "deny") {
148
- const bestMatch = matches[0];
104
+ const recentCallCount = recentLogs.filter(
105
+ (log) => log.timestamp >= oneHourAgo,
106
+ ).length;
107
+ if (recentCallCount >= effectiveHourlyLimit) {
149
108
  return {
150
109
  authorized: false as const,
151
- error: "Function not authorized for this agent",
152
- statusCode: 403,
153
- agentId,
154
- matchedPattern: bestMatch?.functionPattern,
155
- matchedPermission: bestMatch?.permission,
110
+ error: "Rate limit exceeded",
111
+ statusCode: 429,
112
+ retryAfterSeconds: 3600,
113
+ agentId: agent._id,
156
114
  };
157
115
  }
158
116
 
159
- // TODO: For rate_limited permissions, check rate counters here
160
- // (circuit breaker - deferred to post-MVP)
161
-
162
- // 4. Look up function handle
163
- const fnEntry = await ctx.db
164
- .query("functionRegistry")
165
- .withIndex("by_app_and_function", (q) =>
166
- q.eq("appName", args.appName).eq("functionName", args.functionName),
167
- )
168
- .unique();
169
-
170
- if (!fnEntry) {
171
- return {
172
- authorized: false as const,
173
- error: `Function "${args.functionName}" is not registered`,
174
- statusCode: 404,
175
- agentId,
176
- };
117
+ if (
118
+ matchedRule.permission === "rate_limited" &&
119
+ matchedRule.rateLimitConfig?.tokenBudget !== undefined
120
+ ) {
121
+ const estimatedCost = args.estimatedCost ?? 0;
122
+ const tokenEstimate = recentLogs
123
+ .filter((log) => log.timestamp >= oneHourAgo)
124
+ .reduce((sum, log) => sum + estimateCostFromLog(log.args), 0);
125
+ if (tokenEstimate + estimatedCost > matchedRule.rateLimitConfig.tokenBudget) {
126
+ return {
127
+ authorized: false as const,
128
+ error: "Token budget exceeded",
129
+ statusCode: 429,
130
+ retryAfterSeconds: 3600,
131
+ agentId: agent._id,
132
+ };
133
+ }
177
134
  }
178
135
 
179
- // 5. Update activity counters
180
- await ctx.db.patch(instance._id, {
181
- lastActivityAt: Date.now(),
182
- monthlyRequests: instance.monthlyRequests + 1,
136
+ await ctx.db.patch(agent._id, {
137
+ lastUsed: Date.now(),
183
138
  });
184
139
 
185
140
  return {
186
141
  authorized: true as const,
187
- agentId,
188
- appName: args.appName,
189
- functionHandle: fnEntry.functionHandle,
190
- functionType: fnEntry.functionType,
142
+ agentId: agent._id,
191
143
  };
192
144
  },
193
145
  });
@@ -198,23 +150,24 @@ export const authorizeRequest = mutation({
198
150
  */
199
151
  export const logAccess = mutation({
200
152
  args: {
201
- agentId: v.string(),
202
- appName: v.string(),
203
- functionCalled: v.string(),
204
- permission: v.string(),
205
- errorMessage: v.optional(v.string()),
206
- durationMs: v.optional(v.number()),
153
+ agentId: v.id("agents"),
154
+ functionKey: v.string(),
155
+ args: v.any(),
156
+ result: v.optional(v.any()),
157
+ error: v.optional(v.string()),
158
+ duration: v.number(),
159
+ timestamp: v.number(),
207
160
  },
208
161
  returns: v.null(),
209
162
  handler: async (ctx, args) => {
210
- await ctx.db.insert("accessLog", {
211
- timestamp: Date.now(),
163
+ await ctx.db.insert("agentLogs", {
164
+ timestamp: args.timestamp,
212
165
  agentId: args.agentId,
213
- appName: args.appName,
214
- functionCalled: args.functionCalled,
215
- permission: args.permission,
216
- errorMessage: args.errorMessage,
217
- durationMs: args.durationMs,
166
+ functionKey: args.functionKey,
167
+ args: args.args,
168
+ result: args.result,
169
+ error: args.error,
170
+ duration: args.duration,
218
171
  });
219
172
  return null;
220
173
  },
@@ -225,58 +178,91 @@ export const logAccess = mutation({
225
178
  */
226
179
  export const queryAccessLog = query({
227
180
  args: {
228
- agentId: v.optional(v.string()),
229
- appName: v.optional(v.string()),
181
+ agentId: v.optional(v.id("agents")),
182
+ functionKey: v.optional(v.string()),
230
183
  limit: v.optional(v.number()),
231
184
  },
232
185
  returns: v.array(
233
186
  v.object({
187
+ _id: v.id("agentLogs"),
234
188
  timestamp: v.number(),
235
- agentId: v.string(),
236
- appName: v.string(),
237
- functionCalled: v.string(),
238
- permission: v.string(),
239
- errorMessage: v.optional(v.string()),
240
- durationMs: v.optional(v.number()),
189
+ agentId: v.id("agents"),
190
+ functionKey: v.string(),
191
+ args: v.any(),
192
+ result: v.optional(v.any()),
193
+ error: v.optional(v.string()),
194
+ duration: v.number(),
241
195
  }),
242
196
  ),
243
197
  handler: async (ctx, args) => {
244
198
  const limit = args.limit ?? 50;
245
199
 
246
- if (args.agentId) {
200
+ const agentId = args.agentId;
201
+ if (agentId !== undefined) {
247
202
  const logs = await ctx.db
248
- .query("accessLog")
249
- .withIndex("by_agent_and_timestamp", (q) =>
250
- q.eq("agentId", args.agentId!),
203
+ .query("agentLogs")
204
+ .withIndex("by_agentId_and_timestamp", (q) =>
205
+ q.eq("agentId", agentId),
251
206
  )
252
207
  .order("desc")
253
208
  .take(limit);
254
209
 
255
210
  return logs.map((l) => ({
211
+ _id: l._id,
256
212
  timestamp: l.timestamp,
257
213
  agentId: l.agentId,
258
- appName: l.appName,
259
- functionCalled: l.functionCalled,
260
- permission: l.permission,
261
- errorMessage: l.errorMessage,
262
- durationMs: l.durationMs,
214
+ functionKey: l.functionKey,
215
+ args: l.args,
216
+ result: l.result,
217
+ error: l.error,
218
+ duration: l.duration,
263
219
  }));
264
220
  }
265
221
 
266
- // No filter: get recent logs
267
222
  const logs = await ctx.db
268
- .query("accessLog")
223
+ .query("agentLogs")
269
224
  .order("desc")
270
225
  .take(limit);
226
+ const filteredLogs =
227
+ args.functionKey !== undefined
228
+ ? logs.filter((log) => log.functionKey === args.functionKey)
229
+ : logs;
271
230
 
272
- return logs.map((l) => ({
231
+ return filteredLogs.map((l) => ({
232
+ _id: l._id,
273
233
  timestamp: l.timestamp,
274
234
  agentId: l.agentId,
275
- appName: l.appName,
276
- functionCalled: l.functionCalled,
277
- permission: l.permission,
278
- errorMessage: l.errorMessage,
279
- durationMs: l.durationMs,
235
+ functionKey: l.functionKey,
236
+ args: l.args,
237
+ result: l.result,
238
+ error: l.error,
239
+ duration: l.duration,
280
240
  }));
281
241
  },
282
242
  });
243
+
244
+ function resolveEffectiveHourlyLimit(
245
+ baseAgentLimit: number,
246
+ permissionType: PermissionType,
247
+ permissionLimit?: number,
248
+ globalLimit?: number,
249
+ ) {
250
+ let effective = baseAgentLimit;
251
+ if (permissionType === "rate_limited" && permissionLimit !== undefined) {
252
+ effective = Math.min(effective, permissionLimit);
253
+ }
254
+ if (globalLimit !== undefined) {
255
+ effective = Math.min(effective, globalLimit);
256
+ }
257
+ return effective;
258
+ }
259
+
260
+ function estimateCostFromLog(args: unknown): number {
261
+ if (!args || typeof args !== "object") {
262
+ return 0;
263
+ }
264
+ if ("estimatedCost" in args && typeof args.estimatedCost === "number") {
265
+ return args.estimatedCost;
266
+ }
267
+ return 0;
268
+ }