@okrlinkhub/agent-bridge 0.1.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/LICENSE +201 -0
- package/README.md +183 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +184 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +312 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +40 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +224 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/gateway.d.ts +87 -0
- package/dist/component/gateway.d.ts.map +1 -0
- package/dist/component/gateway.js +231 -0
- package/dist/component/gateway.js.map +1 -0
- package/dist/component/permissions.d.ts +93 -0
- package/dist/component/permissions.d.ts.map +1 -0
- package/dist/component/permissions.js +241 -0
- package/dist/component/permissions.js.map +1 -0
- package/dist/component/provisioning.d.ts +87 -0
- package/dist/component/provisioning.d.ts.map +1 -0
- package/dist/component/provisioning.js +343 -0
- package/dist/component/provisioning.js.map +1 -0
- package/dist/component/registry.d.ts +46 -0
- package/dist/component/registry.d.ts.map +1 -0
- package/dist/component/registry.js +121 -0
- package/dist/component/registry.js.map +1 -0
- package/dist/component/schema.d.ts +176 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +92 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +103 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +481 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +56 -0
- package/src/component/_generated/component.ts +281 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/gateway.ts +282 -0
- package/src/component/permissions.ts +321 -0
- package/src/component/provisioning.ts +402 -0
- package/src/component/registry.ts +152 -0
- package/src/component/schema.ts +116 -0
- package/src/react/index.ts +11 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
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 ---
|
|
31
|
+
|
|
32
|
+
const authorizeResultValidator = v.union(
|
|
33
|
+
v.object({
|
|
34
|
+
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
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
v.object({
|
|
45
|
+
authorized: v.literal(false),
|
|
46
|
+
error: v.string(),
|
|
47
|
+
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
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Authorize an agent request.
|
|
62
|
+
* This is a mutation (not a query) because it updates counters and last activity.
|
|
63
|
+
*
|
|
64
|
+
* Steps:
|
|
65
|
+
* 1. Validate instance token
|
|
66
|
+
* 2. Check agent is active
|
|
67
|
+
* 3. Check function permissions
|
|
68
|
+
* 4. Look up function handle in registry
|
|
69
|
+
* 5. Update activity counters
|
|
70
|
+
*
|
|
71
|
+
* Returns the function handle info if authorized, or an error.
|
|
72
|
+
*/
|
|
73
|
+
export const authorizeRequest = mutation({
|
|
74
|
+
args: {
|
|
75
|
+
instanceToken: v.string(),
|
|
76
|
+
functionName: v.string(),
|
|
77
|
+
appName: v.string(),
|
|
78
|
+
},
|
|
79
|
+
returns: authorizeResultValidator,
|
|
80
|
+
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
|
+
)
|
|
88
|
+
.unique();
|
|
89
|
+
|
|
90
|
+
if (!instance) {
|
|
91
|
+
return {
|
|
92
|
+
authorized: false as const,
|
|
93
|
+
error: "Invalid instance token",
|
|
94
|
+
statusCode: 401,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (instance.expiresAt < Date.now()) {
|
|
99
|
+
return {
|
|
100
|
+
authorized: false as const,
|
|
101
|
+
error: "Instance token has expired",
|
|
102
|
+
statusCode: 401,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (instance.appName !== args.appName) {
|
|
107
|
+
return {
|
|
108
|
+
authorized: false as const,
|
|
109
|
+
error: "Token does not match this app",
|
|
110
|
+
statusCode: 403,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
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))
|
|
120
|
+
.unique();
|
|
121
|
+
|
|
122
|
+
if (!agent || !agent.isActive) {
|
|
123
|
+
return {
|
|
124
|
+
authorized: false as const,
|
|
125
|
+
error: "Agent has been revoked",
|
|
126
|
+
statusCode: 403,
|
|
127
|
+
agentId,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
+
)
|
|
137
|
+
.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];
|
|
149
|
+
return {
|
|
150
|
+
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,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
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
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 5. Update activity counters
|
|
180
|
+
await ctx.db.patch(instance._id, {
|
|
181
|
+
lastActivityAt: Date.now(),
|
|
182
|
+
monthlyRequests: instance.monthlyRequests + 1,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
authorized: true as const,
|
|
187
|
+
agentId,
|
|
188
|
+
appName: args.appName,
|
|
189
|
+
functionHandle: fnEntry.functionHandle,
|
|
190
|
+
functionType: fnEntry.functionType,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Log an access attempt to the audit log.
|
|
197
|
+
* Called after function execution (success or failure).
|
|
198
|
+
*/
|
|
199
|
+
export const logAccess = mutation({
|
|
200
|
+
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()),
|
|
207
|
+
},
|
|
208
|
+
returns: v.null(),
|
|
209
|
+
handler: async (ctx, args) => {
|
|
210
|
+
await ctx.db.insert("accessLog", {
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
agentId: args.agentId,
|
|
213
|
+
appName: args.appName,
|
|
214
|
+
functionCalled: args.functionCalled,
|
|
215
|
+
permission: args.permission,
|
|
216
|
+
errorMessage: args.errorMessage,
|
|
217
|
+
durationMs: args.durationMs,
|
|
218
|
+
});
|
|
219
|
+
return null;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Query access logs for audit purposes.
|
|
225
|
+
*/
|
|
226
|
+
export const queryAccessLog = query({
|
|
227
|
+
args: {
|
|
228
|
+
agentId: v.optional(v.string()),
|
|
229
|
+
appName: v.optional(v.string()),
|
|
230
|
+
limit: v.optional(v.number()),
|
|
231
|
+
},
|
|
232
|
+
returns: v.array(
|
|
233
|
+
v.object({
|
|
234
|
+
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()),
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
handler: async (ctx, args) => {
|
|
244
|
+
const limit = args.limit ?? 50;
|
|
245
|
+
|
|
246
|
+
if (args.agentId) {
|
|
247
|
+
const logs = await ctx.db
|
|
248
|
+
.query("accessLog")
|
|
249
|
+
.withIndex("by_agent_and_timestamp", (q) =>
|
|
250
|
+
q.eq("agentId", args.agentId!),
|
|
251
|
+
)
|
|
252
|
+
.order("desc")
|
|
253
|
+
.take(limit);
|
|
254
|
+
|
|
255
|
+
return logs.map((l) => ({
|
|
256
|
+
timestamp: l.timestamp,
|
|
257
|
+
agentId: l.agentId,
|
|
258
|
+
appName: l.appName,
|
|
259
|
+
functionCalled: l.functionCalled,
|
|
260
|
+
permission: l.permission,
|
|
261
|
+
errorMessage: l.errorMessage,
|
|
262
|
+
durationMs: l.durationMs,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// No filter: get recent logs
|
|
267
|
+
const logs = await ctx.db
|
|
268
|
+
.query("accessLog")
|
|
269
|
+
.order("desc")
|
|
270
|
+
.take(limit);
|
|
271
|
+
|
|
272
|
+
return logs.map((l) => ({
|
|
273
|
+
timestamp: l.timestamp,
|
|
274
|
+
agentId: l.agentId,
|
|
275
|
+
appName: l.appName,
|
|
276
|
+
functionCalled: l.functionCalled,
|
|
277
|
+
permission: l.permission,
|
|
278
|
+
errorMessage: l.errorMessage,
|
|
279
|
+
durationMs: l.durationMs,
|
|
280
|
+
}));
|
|
281
|
+
},
|
|
282
|
+
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, internalQuery } from "./_generated/server.js";
|
|
3
|
+
|
|
4
|
+
// --- Permission result type ---
|
|
5
|
+
|
|
6
|
+
const permissionResultValidator = v.object({
|
|
7
|
+
permission: v.union(
|
|
8
|
+
v.literal("allow"),
|
|
9
|
+
v.literal("deny"),
|
|
10
|
+
v.literal("rate_limited"),
|
|
11
|
+
),
|
|
12
|
+
rateLimitConfig: v.optional(
|
|
13
|
+
v.object({
|
|
14
|
+
requestsPerHour: v.number(),
|
|
15
|
+
tokenBudget: v.number(),
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
matchedPattern: v.optional(v.string()),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// --- Pattern matching utilities ---
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculate specificity score for a pattern.
|
|
25
|
+
* More specific patterns (longer fixed prefix before the first wildcard)
|
|
26
|
+
* get higher scores.
|
|
27
|
+
*/
|
|
28
|
+
function patternSpecificity(pattern: string): number {
|
|
29
|
+
const wildcardIndex = pattern.indexOf("*");
|
|
30
|
+
if (wildcardIndex === -1) return pattern.length;
|
|
31
|
+
return wildcardIndex;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a function name matches a permission pattern.
|
|
36
|
+
* Supports "*" as a wildcard that matches any characters.
|
|
37
|
+
* Examples: "okr:*" matches "okr:getObjectives", "*" matches anything.
|
|
38
|
+
*/
|
|
39
|
+
function matchesPattern(functionName: string, pattern: string): boolean {
|
|
40
|
+
if (pattern === "*") return true;
|
|
41
|
+
// Escape regex special chars except *, then replace * with .*
|
|
42
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
|
|
44
|
+
const regex = new RegExp(regexStr);
|
|
45
|
+
return regex.test(functionName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Public functions ---
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set a permission for an agent on a specific app.
|
|
52
|
+
* If a permission with the same agentId + appName + functionPattern exists, it is updated.
|
|
53
|
+
*/
|
|
54
|
+
export const setPermission = mutation({
|
|
55
|
+
args: {
|
|
56
|
+
agentId: v.string(),
|
|
57
|
+
appName: v.string(),
|
|
58
|
+
functionPattern: v.string(),
|
|
59
|
+
permission: v.union(
|
|
60
|
+
v.literal("allow"),
|
|
61
|
+
v.literal("deny"),
|
|
62
|
+
v.literal("rate_limited"),
|
|
63
|
+
),
|
|
64
|
+
rateLimitConfig: v.optional(
|
|
65
|
+
v.object({
|
|
66
|
+
requestsPerHour: v.number(),
|
|
67
|
+
tokenBudget: v.number(),
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
createdBy: v.string(),
|
|
71
|
+
},
|
|
72
|
+
returns: v.string(),
|
|
73
|
+
handler: async (ctx, args) => {
|
|
74
|
+
// Check for existing permission with same pattern
|
|
75
|
+
const existing = await ctx.db
|
|
76
|
+
.query("functionPermissions")
|
|
77
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
78
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
79
|
+
)
|
|
80
|
+
.collect();
|
|
81
|
+
|
|
82
|
+
const match = existing.find(
|
|
83
|
+
(p) => p.functionPattern === args.functionPattern,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (match) {
|
|
87
|
+
await ctx.db.patch(match._id, {
|
|
88
|
+
permission: args.permission,
|
|
89
|
+
rateLimitConfig: args.rateLimitConfig,
|
|
90
|
+
createdBy: args.createdBy,
|
|
91
|
+
createdAt: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
return match._id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const id = await ctx.db.insert("functionPermissions", {
|
|
97
|
+
agentId: args.agentId,
|
|
98
|
+
appName: args.appName,
|
|
99
|
+
functionPattern: args.functionPattern,
|
|
100
|
+
permission: args.permission,
|
|
101
|
+
rateLimitConfig: args.rateLimitConfig,
|
|
102
|
+
createdAt: Date.now(),
|
|
103
|
+
createdBy: args.createdBy,
|
|
104
|
+
});
|
|
105
|
+
return id;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove a specific permission.
|
|
111
|
+
*/
|
|
112
|
+
export const removePermission = mutation({
|
|
113
|
+
args: {
|
|
114
|
+
agentId: v.string(),
|
|
115
|
+
appName: v.string(),
|
|
116
|
+
functionPattern: v.string(),
|
|
117
|
+
},
|
|
118
|
+
returns: v.boolean(),
|
|
119
|
+
handler: async (ctx, args) => {
|
|
120
|
+
const perms = await ctx.db
|
|
121
|
+
.query("functionPermissions")
|
|
122
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
123
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
124
|
+
)
|
|
125
|
+
.collect();
|
|
126
|
+
|
|
127
|
+
const match = perms.find(
|
|
128
|
+
(p) => p.functionPattern === args.functionPattern,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!match) return false;
|
|
132
|
+
|
|
133
|
+
await ctx.db.delete(match._id);
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check permission for a specific function call.
|
|
140
|
+
* Applies pattern matching with specificity ordering (most specific pattern wins).
|
|
141
|
+
* Default: deny if no matching pattern is found.
|
|
142
|
+
*/
|
|
143
|
+
export const checkPermission = query({
|
|
144
|
+
args: {
|
|
145
|
+
agentId: v.string(),
|
|
146
|
+
appName: v.string(),
|
|
147
|
+
functionName: v.string(),
|
|
148
|
+
},
|
|
149
|
+
returns: permissionResultValidator,
|
|
150
|
+
handler: async (ctx, args) => {
|
|
151
|
+
const permissions = await ctx.db
|
|
152
|
+
.query("functionPermissions")
|
|
153
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
154
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
155
|
+
)
|
|
156
|
+
.collect();
|
|
157
|
+
|
|
158
|
+
// Find all matching patterns and sort by specificity (most specific first)
|
|
159
|
+
const matches = permissions
|
|
160
|
+
.filter((p) => matchesPattern(args.functionName, p.functionPattern))
|
|
161
|
+
.sort(
|
|
162
|
+
(a, b) =>
|
|
163
|
+
patternSpecificity(b.functionPattern) -
|
|
164
|
+
patternSpecificity(a.functionPattern),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (matches.length === 0) {
|
|
168
|
+
// Default: deny
|
|
169
|
+
return { permission: "deny" as const };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Most specific pattern wins
|
|
173
|
+
const best = matches[0];
|
|
174
|
+
return {
|
|
175
|
+
permission: best.permission,
|
|
176
|
+
rateLimitConfig: best.rateLimitConfig,
|
|
177
|
+
matchedPattern: best.functionPattern,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* List all permissions for an agent on a specific app.
|
|
184
|
+
*/
|
|
185
|
+
export const listPermissions = query({
|
|
186
|
+
args: {
|
|
187
|
+
agentId: v.string(),
|
|
188
|
+
appName: v.string(),
|
|
189
|
+
},
|
|
190
|
+
returns: v.array(
|
|
191
|
+
v.object({
|
|
192
|
+
functionPattern: v.string(),
|
|
193
|
+
permission: v.union(
|
|
194
|
+
v.literal("allow"),
|
|
195
|
+
v.literal("deny"),
|
|
196
|
+
v.literal("rate_limited"),
|
|
197
|
+
),
|
|
198
|
+
rateLimitConfig: v.optional(
|
|
199
|
+
v.object({
|
|
200
|
+
requestsPerHour: v.number(),
|
|
201
|
+
tokenBudget: v.number(),
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
createdAt: v.number(),
|
|
205
|
+
createdBy: v.string(),
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
handler: async (ctx, args) => {
|
|
209
|
+
const perms = await ctx.db
|
|
210
|
+
.query("functionPermissions")
|
|
211
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
212
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
213
|
+
)
|
|
214
|
+
.collect();
|
|
215
|
+
|
|
216
|
+
return perms.map((p) => ({
|
|
217
|
+
functionPattern: p.functionPattern,
|
|
218
|
+
permission: p.permission,
|
|
219
|
+
rateLimitConfig: p.rateLimitConfig,
|
|
220
|
+
createdAt: p.createdAt,
|
|
221
|
+
createdBy: p.createdBy,
|
|
222
|
+
}));
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Remove all permissions for a specific agent on a specific app.
|
|
228
|
+
*/
|
|
229
|
+
export const clearPermissions = mutation({
|
|
230
|
+
args: {
|
|
231
|
+
agentId: v.string(),
|
|
232
|
+
appName: v.string(),
|
|
233
|
+
},
|
|
234
|
+
returns: v.number(),
|
|
235
|
+
handler: async (ctx, args) => {
|
|
236
|
+
const perms = await ctx.db
|
|
237
|
+
.query("functionPermissions")
|
|
238
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
239
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
240
|
+
)
|
|
241
|
+
.collect();
|
|
242
|
+
|
|
243
|
+
for (const perm of perms) {
|
|
244
|
+
await ctx.db.delete(perm._id);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return perms.length;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Debug helper: show permission matching for a specific function call.
|
|
253
|
+
*/
|
|
254
|
+
export const debugMatchPermission = query({
|
|
255
|
+
args: {
|
|
256
|
+
agentId: v.string(),
|
|
257
|
+
appName: v.string(),
|
|
258
|
+
functionName: v.string(),
|
|
259
|
+
},
|
|
260
|
+
returns: v.object({
|
|
261
|
+
functionName: v.string(),
|
|
262
|
+
permissions: v.array(
|
|
263
|
+
v.object({
|
|
264
|
+
functionPattern: v.string(),
|
|
265
|
+
permission: v.union(
|
|
266
|
+
v.literal("allow"),
|
|
267
|
+
v.literal("deny"),
|
|
268
|
+
v.literal("rate_limited"),
|
|
269
|
+
),
|
|
270
|
+
specificity: v.number(),
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
matches: v.array(
|
|
274
|
+
v.object({
|
|
275
|
+
functionPattern: v.string(),
|
|
276
|
+
permission: v.union(
|
|
277
|
+
v.literal("allow"),
|
|
278
|
+
v.literal("deny"),
|
|
279
|
+
v.literal("rate_limited"),
|
|
280
|
+
),
|
|
281
|
+
specificity: v.number(),
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
284
|
+
bestMatch: v.optional(
|
|
285
|
+
v.object({
|
|
286
|
+
functionPattern: v.string(),
|
|
287
|
+
permission: v.union(
|
|
288
|
+
v.literal("allow"),
|
|
289
|
+
v.literal("deny"),
|
|
290
|
+
v.literal("rate_limited"),
|
|
291
|
+
),
|
|
292
|
+
specificity: v.number(),
|
|
293
|
+
}),
|
|
294
|
+
),
|
|
295
|
+
}),
|
|
296
|
+
handler: async (ctx, args) => {
|
|
297
|
+
const permissions = await ctx.db
|
|
298
|
+
.query("functionPermissions")
|
|
299
|
+
.withIndex("by_agent_and_app", (q) =>
|
|
300
|
+
q.eq("agentId", args.agentId).eq("appName", args.appName),
|
|
301
|
+
)
|
|
302
|
+
.collect();
|
|
303
|
+
|
|
304
|
+
const withSpecificity = permissions.map((p) => ({
|
|
305
|
+
functionPattern: p.functionPattern,
|
|
306
|
+
permission: p.permission,
|
|
307
|
+
specificity: patternSpecificity(p.functionPattern),
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
const matches = withSpecificity
|
|
311
|
+
.filter((p) => matchesPattern(args.functionName, p.functionPattern))
|
|
312
|
+
.sort((a, b) => b.specificity - a.specificity);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
functionName: args.functionName,
|
|
316
|
+
permissions: withSpecificity,
|
|
317
|
+
matches,
|
|
318
|
+
bestMatch: matches[0],
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
});
|