@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.
- package/README.md +96 -127
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +100 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/client/index.d.ts +50 -173
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +129 -263
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +4 -4
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +66 -162
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/agentBridgeUtils.d.ts +8 -0
- package/dist/component/agentBridgeUtils.d.ts.map +1 -0
- package/dist/component/agentBridgeUtils.js +33 -0
- package/dist/component/agentBridgeUtils.js.map +1 -0
- package/dist/component/agents.d.ts +27 -0
- package/dist/component/agents.d.ts.map +1 -0
- package/dist/component/agents.js +94 -0
- package/dist/component/agents.js.map +1 -0
- package/dist/component/gateway.d.ts +30 -44
- package/dist/component/gateway.d.ts.map +1 -1
- package/dist/component/gateway.js +127 -132
- package/dist/component/gateway.js.map +1 -1
- package/dist/component/permissions.d.ts +30 -84
- package/dist/component/permissions.d.ts.map +1 -1
- package/dist/component/permissions.js +80 -203
- package/dist/component/permissions.js.map +1 -1
- package/dist/component/schema.d.ts +55 -153
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +30 -80
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -3
- package/dist/react/index.js.map +1 -1
- package/package.json +7 -3
- package/src/cli/init.ts +116 -0
- package/src/client/index.ts +228 -389
- package/src/component/_generated/api.ts +4 -4
- package/src/component/_generated/component.ts +79 -195
- package/src/component/agentBridgeUtils.ts +52 -0
- package/src/component/agents.ts +106 -0
- package/src/component/gateway.ts +149 -163
- package/src/component/permissions.ts +89 -259
- package/src/component/schema.ts +31 -96
- package/src/react/index.ts +5 -6
- package/dist/component/provisioning.d.ts +0 -87
- package/dist/component/provisioning.d.ts.map +0 -1
- package/dist/component/provisioning.js +0 -343
- package/dist/component/provisioning.js.map +0 -1
- package/dist/component/registry.d.ts +0 -46
- package/dist/component/registry.d.ts.map +0 -1
- package/dist/component/registry.js +0 -121
- package/dist/component/registry.js.map +0 -1
- package/src/component/provisioning.ts +0 -402
- package/src/component/registry.ts +0 -152
package/src/component/gateway.ts
CHANGED
|
@@ -1,59 +1,22 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation, query } from "./_generated/server.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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.
|
|
49
|
-
|
|
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
|
|
28
|
+
* 1. Validate API key
|
|
66
29
|
* 2. Check agent is active
|
|
67
30
|
* 3. Check function permissions
|
|
68
|
-
* 4.
|
|
69
|
-
* 5.
|
|
31
|
+
* 4. Check function global override
|
|
32
|
+
* 5. Check rate limits
|
|
70
33
|
*
|
|
71
|
-
* Returns the
|
|
34
|
+
* Returns the agent id if authorized, or an error.
|
|
72
35
|
*/
|
|
73
36
|
export const authorizeRequest = mutation({
|
|
74
37
|
args: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
.
|
|
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
|
|
52
|
+
error: "Invalid API key",
|
|
94
53
|
statusCode: 401,
|
|
95
54
|
};
|
|
96
55
|
}
|
|
97
56
|
|
|
98
|
-
if (
|
|
57
|
+
if (!agent.enabled) {
|
|
99
58
|
return {
|
|
100
59
|
authorized: false as const,
|
|
101
|
-
error: "
|
|
102
|
-
statusCode:
|
|
60
|
+
error: "Agent disabled",
|
|
61
|
+
statusCode: 403,
|
|
62
|
+
agentId: agent._id,
|
|
103
63
|
};
|
|
104
64
|
}
|
|
105
65
|
|
|
106
|
-
|
|
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:
|
|
74
|
+
error: `Function ${args.functionKey} not allowed`,
|
|
110
75
|
statusCode: 403,
|
|
76
|
+
agentId: agent._id,
|
|
111
77
|
};
|
|
112
78
|
}
|
|
113
79
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
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:
|
|
87
|
+
error: `Function ${args.functionKey} disabled`,
|
|
126
88
|
statusCode: 403,
|
|
127
|
-
agentId,
|
|
89
|
+
agentId: agent._id,
|
|
128
90
|
};
|
|
129
91
|
}
|
|
130
92
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
.
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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: "
|
|
152
|
-
statusCode:
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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("
|
|
211
|
-
timestamp:
|
|
163
|
+
await ctx.db.insert("agentLogs", {
|
|
164
|
+
timestamp: args.timestamp,
|
|
212
165
|
agentId: args.agentId,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
229
|
-
|
|
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.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
200
|
+
const agentId = args.agentId;
|
|
201
|
+
if (agentId !== undefined) {
|
|
247
202
|
const logs = await ctx.db
|
|
248
|
-
.query("
|
|
249
|
-
.withIndex("
|
|
250
|
-
q.eq("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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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("
|
|
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
|
|
231
|
+
return filteredLogs.map((l) => ({
|
|
232
|
+
_id: l._id,
|
|
273
233
|
timestamp: l.timestamp,
|
|
274
234
|
agentId: l.agentId,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|