@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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +183 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +184 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +312 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +40 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +224 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +3 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/gateway.d.ts +87 -0
  32. package/dist/component/gateway.d.ts.map +1 -0
  33. package/dist/component/gateway.js +231 -0
  34. package/dist/component/gateway.js.map +1 -0
  35. package/dist/component/permissions.d.ts +93 -0
  36. package/dist/component/permissions.d.ts.map +1 -0
  37. package/dist/component/permissions.js +241 -0
  38. package/dist/component/permissions.js.map +1 -0
  39. package/dist/component/provisioning.d.ts +87 -0
  40. package/dist/component/provisioning.d.ts.map +1 -0
  41. package/dist/component/provisioning.js +343 -0
  42. package/dist/component/provisioning.js.map +1 -0
  43. package/dist/component/registry.d.ts +46 -0
  44. package/dist/component/registry.d.ts.map +1 -0
  45. package/dist/component/registry.js +121 -0
  46. package/dist/component/registry.js.map +1 -0
  47. package/dist/component/schema.d.ts +176 -0
  48. package/dist/component/schema.d.ts.map +1 -0
  49. package/dist/component/schema.js +92 -0
  50. package/dist/component/schema.js.map +1 -0
  51. package/dist/react/index.d.ts +3 -0
  52. package/dist/react/index.d.ts.map +1 -0
  53. package/dist/react/index.js +5 -0
  54. package/dist/react/index.js.map +1 -0
  55. package/package.json +103 -0
  56. package/src/client/_generated/_ignore.ts +1 -0
  57. package/src/client/index.ts +481 -0
  58. package/src/client/setup.test.ts +26 -0
  59. package/src/component/_generated/api.ts +56 -0
  60. package/src/component/_generated/component.ts +281 -0
  61. package/src/component/_generated/dataModel.ts +60 -0
  62. package/src/component/_generated/server.ts +156 -0
  63. package/src/component/convex.config.ts +3 -0
  64. package/src/component/gateway.ts +282 -0
  65. package/src/component/permissions.ts +321 -0
  66. package/src/component/provisioning.ts +402 -0
  67. package/src/component/registry.ts +152 -0
  68. package/src/component/schema.ts +116 -0
  69. package/src/react/index.ts +11 -0
  70. package/src/test.ts +18 -0
@@ -0,0 +1,481 @@
1
+ import { httpActionGeneric } from "convex/server";
2
+ import type {
3
+ GenericActionCtx,
4
+ GenericDataModel,
5
+ GenericMutationCtx,
6
+ GenericQueryCtx,
7
+ FunctionHandle,
8
+ HttpRouter,
9
+ } from "convex/server";
10
+ import type { ComponentApi } from "../component/_generated/component.js";
11
+
12
+ // Convenient context types with minimal required capabilities.
13
+ type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
14
+ type MutationCtx = Pick<
15
+ GenericMutationCtx<GenericDataModel>,
16
+ "runQuery" | "runMutation"
17
+ >;
18
+ type ActionCtx = Pick<
19
+ GenericActionCtx<GenericDataModel>,
20
+ "runQuery" | "runMutation" | "runAction"
21
+ >;
22
+
23
+ // --- Types ---
24
+
25
+ export interface FunctionDef {
26
+ /** Alias name that agents use to call this function (e.g. "okr:getObjectives") */
27
+ name: string;
28
+ /** Function handle string from createFunctionHandle() */
29
+ handle: string;
30
+ /** Type of the Convex function */
31
+ type: "query" | "mutation" | "action";
32
+ /** Human-readable description */
33
+ description?: string;
34
+ }
35
+
36
+ export interface DefaultPermission {
37
+ pattern: string;
38
+ permission: "allow" | "deny" | "rate_limited";
39
+ rateLimitConfig?: {
40
+ requestsPerHour: number;
41
+ tokenBudget: number;
42
+ };
43
+ }
44
+
45
+ export interface AgentBridgeConfig {
46
+ /** Unique name identifying this app (e.g. "okr", "hr", "incentives") */
47
+ appName: string;
48
+ /** Default permissions applied to new agents during provisioning */
49
+ defaultPermissions?: DefaultPermission[];
50
+ }
51
+
52
+ // --- AgentBridge Client Class ---
53
+
54
+ /**
55
+ * Client class that wraps component calls for ergonomic use in the host app.
56
+ *
57
+ * Usage:
58
+ * ```ts
59
+ * import { AgentBridge } from "@okrlinkhub/agent-bridge";
60
+ * import { components } from "./_generated/api";
61
+ *
62
+ * const bridge = new AgentBridge(components.agentBridge, { appName: "okr" });
63
+ * ```
64
+ */
65
+ export class AgentBridge {
66
+ public component: ComponentApi;
67
+ public appName: string;
68
+ private defaultPermissions: DefaultPermission[];
69
+
70
+ constructor(component: ComponentApi, config: AgentBridgeConfig) {
71
+ this.component = component;
72
+ this.appName = config.appName;
73
+ this.defaultPermissions = config.defaultPermissions ?? [
74
+ { pattern: "*", permission: "deny" },
75
+ ];
76
+ }
77
+
78
+ /**
79
+ * Initialize the component configuration.
80
+ * Should be called once during app setup (e.g. in a seed/setup mutation).
81
+ */
82
+ async configure(ctx: MutationCtx): Promise<void> {
83
+ await ctx.runMutation(this.component.provisioning.configure, {
84
+ appName: this.appName,
85
+ defaultPermissions: this.defaultPermissions,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Register a function that agents can call.
91
+ * The host app must create the function handle via createFunctionHandle().
92
+ */
93
+ async registerFunction(ctx: MutationCtx, fn: FunctionDef): Promise<string> {
94
+ return await ctx.runMutation(this.component.registry.register, {
95
+ appName: this.appName,
96
+ functionName: fn.name,
97
+ functionHandle: fn.handle,
98
+ functionType: fn.type,
99
+ description: fn.description,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Register multiple functions in bulk.
105
+ */
106
+ async registerFunctions(
107
+ ctx: MutationCtx,
108
+ functions: FunctionDef[],
109
+ ): Promise<string[]> {
110
+ const ids: string[] = [];
111
+ for (const fn of functions) {
112
+ const id = await this.registerFunction(ctx, fn);
113
+ ids.push(id);
114
+ }
115
+ return ids;
116
+ }
117
+
118
+ /**
119
+ * Unregister a function.
120
+ */
121
+ async unregisterFunction(
122
+ ctx: MutationCtx,
123
+ functionName: string,
124
+ ): Promise<boolean> {
125
+ return await ctx.runMutation(this.component.registry.unregister, {
126
+ appName: this.appName,
127
+ functionName,
128
+ });
129
+ }
130
+
131
+ /**
132
+ * List all registered functions for this app.
133
+ */
134
+ async listFunctions(ctx: QueryCtx) {
135
+ return await ctx.runQuery(this.component.registry.listFunctions, {
136
+ appName: this.appName,
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Generate a provisioning token (admin operation).
142
+ * Returns the plaintext token -- store/communicate securely!
143
+ */
144
+ async generateProvisioningToken(
145
+ ctx: MutationCtx,
146
+ opts: {
147
+ employeeEmail: string;
148
+ department: string;
149
+ maxApps?: number;
150
+ expiresInDays?: number;
151
+ createdBy: string;
152
+ },
153
+ ) {
154
+ return await ctx.runMutation(
155
+ this.component.provisioning.generateProvisioningToken,
156
+ opts,
157
+ );
158
+ }
159
+
160
+ /**
161
+ * List all registered agents.
162
+ */
163
+ async listAgents(ctx: QueryCtx, activeOnly?: boolean) {
164
+ return await ctx.runQuery(this.component.provisioning.listAgents, {
165
+ activeOnly,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Revoke an agent globally.
171
+ */
172
+ async revokeAgent(
173
+ ctx: MutationCtx,
174
+ opts: { agentId: string; revokedBy: string },
175
+ ): Promise<boolean> {
176
+ return await ctx.runMutation(this.component.provisioning.revokeAgent, opts);
177
+ }
178
+
179
+ /**
180
+ * Revoke a specific app instance for an agent.
181
+ */
182
+ async revokeAppInstance(
183
+ ctx: MutationCtx,
184
+ opts: { agentId: string },
185
+ ): Promise<boolean> {
186
+ return await ctx.runMutation(
187
+ this.component.provisioning.revokeAppInstance,
188
+ {
189
+ agentId: opts.agentId,
190
+ appName: this.appName,
191
+ },
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Set a permission for an agent.
197
+ */
198
+ async setPermission(
199
+ ctx: MutationCtx,
200
+ opts: {
201
+ agentId: string;
202
+ functionPattern: string;
203
+ permission: "allow" | "deny" | "rate_limited";
204
+ rateLimitConfig?: { requestsPerHour: number; tokenBudget: number };
205
+ createdBy: string;
206
+ },
207
+ ): Promise<string> {
208
+ return await ctx.runMutation(this.component.permissions.setPermission, {
209
+ agentId: opts.agentId,
210
+ appName: this.appName,
211
+ functionPattern: opts.functionPattern,
212
+ permission: opts.permission,
213
+ rateLimitConfig: opts.rateLimitConfig,
214
+ createdBy: opts.createdBy,
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Remove a permission for an agent.
220
+ */
221
+ async removePermission(
222
+ ctx: MutationCtx,
223
+ opts: { agentId: string; functionPattern: string },
224
+ ): Promise<boolean> {
225
+ return await ctx.runMutation(this.component.permissions.removePermission, {
226
+ agentId: opts.agentId,
227
+ appName: this.appName,
228
+ functionPattern: opts.functionPattern,
229
+ });
230
+ }
231
+
232
+ /**
233
+ * List permissions for an agent on this app.
234
+ */
235
+ async listPermissions(ctx: QueryCtx, agentId: string) {
236
+ return await ctx.runQuery(this.component.permissions.listPermissions, {
237
+ agentId,
238
+ appName: this.appName,
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Query access logs.
244
+ */
245
+ async queryAccessLog(
246
+ ctx: QueryCtx,
247
+ opts?: { agentId?: string; limit?: number },
248
+ ) {
249
+ return await ctx.runQuery(this.component.gateway.queryAccessLog, {
250
+ agentId: opts?.agentId,
251
+ appName: this.appName,
252
+ limit: opts?.limit,
253
+ });
254
+ }
255
+ }
256
+
257
+ // --- HTTP Route Registration ---
258
+
259
+ /**
260
+ * Register HTTP routes for the agent bridge component.
261
+ * This exposes endpoints that OpenClaw agents call to execute functions,
262
+ * provision themselves, and check health.
263
+ *
264
+ * Must be called in the host app's `convex/http.ts` file.
265
+ *
266
+ * @example
267
+ * ```ts
268
+ * import { httpRouter } from "convex/server";
269
+ * import { registerRoutes } from "@okrlinkhub/agent-bridge";
270
+ * import { components } from "./_generated/api";
271
+ *
272
+ * const http = httpRouter();
273
+ * registerRoutes(http, components.agentBridge, { appName: "okr" });
274
+ * export default http;
275
+ * ```
276
+ */
277
+ export function registerRoutes(
278
+ http: HttpRouter,
279
+ component: ComponentApi,
280
+ config: { appName: string; pathPrefix?: string },
281
+ ) {
282
+ const prefix = config.pathPrefix ?? "/agent-bridge";
283
+
284
+ // --- POST /agent-bridge/execute ---
285
+ // Gateway: execute a registered function on behalf of an agent
286
+ http.route({
287
+ path: `${prefix}/execute`,
288
+ method: "POST",
289
+ handler: httpActionGeneric(async (ctx, request) => {
290
+ let body: {
291
+ instanceToken?: string;
292
+ functionName?: string;
293
+ args?: Record<string, unknown>;
294
+ };
295
+ try {
296
+ body = await request.json();
297
+ } catch {
298
+ return jsonResponse({ error: "Invalid JSON body" }, 400);
299
+ }
300
+
301
+ const { instanceToken, functionName, args } = body;
302
+
303
+ if (!instanceToken || !functionName) {
304
+ return jsonResponse(
305
+ { error: "Missing required fields: instanceToken, functionName" },
306
+ 400,
307
+ );
308
+ }
309
+
310
+ // Step 1: Authorize the request (mutation -- validates token, checks permissions,
311
+ // updates counters, returns function handle)
312
+ const authResult = await ctx.runMutation(
313
+ component.gateway.authorizeRequest,
314
+ {
315
+ instanceToken,
316
+ functionName,
317
+ appName: config.appName,
318
+ },
319
+ );
320
+
321
+ if (!authResult.authorized) {
322
+ const detailSuffix =
323
+ authResult.matchedPattern && authResult.matchedPermission
324
+ ? ` (matchedPattern="${authResult.matchedPattern}", permission="${authResult.matchedPermission}")`
325
+ : "";
326
+ // Log the denied access
327
+ await ctx.runMutation(component.gateway.logAccess, {
328
+ agentId: authResult.agentId ?? "unknown",
329
+ appName: config.appName,
330
+ functionCalled: functionName,
331
+ permission: "deny",
332
+ errorMessage: authResult.error + detailSuffix,
333
+ });
334
+
335
+ return jsonResponse(
336
+ { error: authResult.error },
337
+ authResult.statusCode,
338
+ );
339
+ }
340
+
341
+ // Step 2: Execute the function via the registered handle
342
+ const startTime = Date.now();
343
+ try {
344
+ let result: unknown;
345
+
346
+ switch (authResult.functionType) {
347
+ case "query":
348
+ result = await ctx.runQuery(
349
+ authResult.functionHandle as FunctionHandle<"query">,
350
+ args ?? {},
351
+ );
352
+ break;
353
+ case "mutation":
354
+ result = await ctx.runMutation(
355
+ authResult.functionHandle as FunctionHandle<"mutation">,
356
+ args ?? {},
357
+ );
358
+ break;
359
+ case "action":
360
+ result = await ctx.runAction(
361
+ authResult.functionHandle as FunctionHandle<"action">,
362
+ args ?? {},
363
+ );
364
+ break;
365
+ }
366
+
367
+ const durationMs = Date.now() - startTime;
368
+
369
+ // Step 3: Log successful access
370
+ await ctx.runMutation(component.gateway.logAccess, {
371
+ agentId: authResult.agentId,
372
+ appName: config.appName,
373
+ functionCalled: functionName,
374
+ permission: "allow",
375
+ durationMs,
376
+ });
377
+
378
+ return jsonResponse({ result, durationMs }, 200);
379
+ } catch (error: unknown) {
380
+ const durationMs = Date.now() - startTime;
381
+ const errorMessage =
382
+ error instanceof Error ? error.message : "Unknown error";
383
+
384
+ // Log the failed execution
385
+ await ctx.runMutation(component.gateway.logAccess, {
386
+ agentId: authResult.agentId,
387
+ appName: config.appName,
388
+ functionCalled: functionName,
389
+ permission: "allow",
390
+ errorMessage,
391
+ durationMs,
392
+ });
393
+
394
+ return jsonResponse({ error: errorMessage }, 500);
395
+ }
396
+ }),
397
+ });
398
+
399
+ // --- POST /agent-bridge/provision ---
400
+ // Agent self-provisioning endpoint
401
+ http.route({
402
+ path: `${prefix}/provision`,
403
+ method: "POST",
404
+ handler: httpActionGeneric(async (ctx, request) => {
405
+ let body: { provisioningToken?: string };
406
+ try {
407
+ body = await request.json();
408
+ } catch {
409
+ return jsonResponse({ error: "Invalid JSON body" }, 400);
410
+ }
411
+
412
+ const { provisioningToken } = body;
413
+
414
+ if (!provisioningToken) {
415
+ return jsonResponse(
416
+ { error: "Missing required field: provisioningToken" },
417
+ 400,
418
+ );
419
+ }
420
+
421
+ try {
422
+ const result = await ctx.runMutation(
423
+ component.provisioning.provisionAgent,
424
+ {
425
+ provisioningToken,
426
+ appName: config.appName,
427
+ },
428
+ );
429
+
430
+ return jsonResponse(result, 200);
431
+ } catch (error: unknown) {
432
+ const errorMessage =
433
+ error instanceof Error ? error.message : "Provisioning failed";
434
+ return jsonResponse({ error: errorMessage }, 400);
435
+ }
436
+ }),
437
+ });
438
+
439
+ // --- GET /agent-bridge/health ---
440
+ // Health check endpoint
441
+ http.route({
442
+ path: `${prefix}/health`,
443
+ method: "GET",
444
+ handler: httpActionGeneric(async (ctx) => {
445
+ try {
446
+ const functions = await ctx.runQuery(
447
+ component.registry.listFunctions,
448
+ { appName: config.appName },
449
+ );
450
+
451
+ return jsonResponse(
452
+ {
453
+ status: "ok",
454
+ appName: config.appName,
455
+ registeredFunctions: functions.length,
456
+ timestamp: Date.now(),
457
+ },
458
+ 200,
459
+ );
460
+ } catch {
461
+ return jsonResponse(
462
+ {
463
+ status: "error",
464
+ appName: config.appName,
465
+ timestamp: Date.now(),
466
+ },
467
+ 500,
468
+ );
469
+ }
470
+ }),
471
+ });
472
+ }
473
+
474
+ // --- Helper ---
475
+
476
+ function jsonResponse(data: unknown, status: number): Response {
477
+ return new Response(JSON.stringify(data), {
478
+ status,
479
+ headers: { "Content-Type": "application/json" },
480
+ });
481
+ }
@@ -0,0 +1,26 @@
1
+ /// <reference types="vite/client" />
2
+ import { test } from "vitest";
3
+ import { convexTest } from "convex-test";
4
+ export const modules = import.meta.glob("./**/*.*s");
5
+
6
+ import {
7
+ defineSchema,
8
+ type GenericSchema,
9
+ type SchemaDefinition,
10
+ } from "convex/server";
11
+ import { type ComponentApi } from "../component/_generated/component.js";
12
+ import { componentsGeneric } from "convex/server";
13
+ import { register } from "../test.js";
14
+
15
+ export function initConvexTest<
16
+ Schema extends SchemaDefinition<GenericSchema, boolean>,
17
+ >(schema?: Schema) {
18
+ const t = convexTest(schema ?? defineSchema({}), modules);
19
+ register(t);
20
+ return t;
21
+ }
22
+ export const components = componentsGeneric() as unknown as {
23
+ agentBridge: ComponentApi;
24
+ };
25
+
26
+ test("setup", () => {});
@@ -0,0 +1,56 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as gateway from "../gateway.js";
12
+ import type * as permissions from "../permissions.js";
13
+ import type * as provisioning from "../provisioning.js";
14
+ import type * as registry from "../registry.js";
15
+
16
+ import type {
17
+ ApiFromModules,
18
+ FilterApi,
19
+ FunctionReference,
20
+ } from "convex/server";
21
+ import { anyApi, componentsGeneric } from "convex/server";
22
+
23
+ const fullApi: ApiFromModules<{
24
+ gateway: typeof gateway;
25
+ permissions: typeof permissions;
26
+ provisioning: typeof provisioning;
27
+ registry: typeof registry;
28
+ }> = anyApi as any;
29
+
30
+ /**
31
+ * A utility for referencing Convex functions in your app's public API.
32
+ *
33
+ * Usage:
34
+ * ```js
35
+ * const myFunctionReference = api.myModule.myFunction;
36
+ * ```
37
+ */
38
+ export const api: FilterApi<
39
+ typeof fullApi,
40
+ FunctionReference<any, "public">
41
+ > = anyApi as any;
42
+
43
+ /**
44
+ * A utility for referencing Convex functions in your app's internal API.
45
+ *
46
+ * Usage:
47
+ * ```js
48
+ * const myFunctionReference = internal.myModule.myFunction;
49
+ * ```
50
+ */
51
+ export const internal: FilterApi<
52
+ typeof fullApi,
53
+ FunctionReference<any, "internal">
54
+ > = anyApi as any;
55
+
56
+ export const components = componentsGeneric() as unknown as {};