@okrlinkhub/agent-bridge 2.0.1 → 3.0.1

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.
@@ -6,6 +6,24 @@ import type {
6
6
  HttpRouter,
7
7
  } from "convex/server";
8
8
  import type { ComponentApi } from "../component/_generated/component.js";
9
+ export {
10
+ buildAgentBridgeStrictHeaders,
11
+ createAuth0TokenAdapter,
12
+ createCustomOidcTokenAdapter,
13
+ createNextAuthConvexTokenAdapter,
14
+ decodeJwtClaims,
15
+ resolveUserToken,
16
+ validateJwtClaims,
17
+ } from "./userAuth.js";
18
+ export type {
19
+ AgentBridgeStrictHeadersInput,
20
+ JwtClaimValidationOptions,
21
+ JwtClaimValidationResult,
22
+ JwtClaims,
23
+ NextAuthSessionLike,
24
+ TokenSource,
25
+ TokenSourceAdapter,
26
+ } from "./userAuth.js";
9
27
 
10
28
  export type AgentBridgeFunctionType = "query" | "mutation" | "action";
11
29
 
@@ -136,8 +154,8 @@ type ExecuteRequestBody = {
136
154
 
137
155
  type RegisterRoutesOptions = {
138
156
  pathPrefix?: string;
139
- serviceKey?: string;
140
- serviceKeyEnvVar?: string;
157
+ serviceKeys?: Record<string, string>;
158
+ serviceKeysEnvVar?: string;
141
159
  };
142
160
 
143
161
  export function registerRoutes(
@@ -147,9 +165,10 @@ export function registerRoutes(
147
165
  options?: RegisterRoutesOptions,
148
166
  ) {
149
167
  const prefix = options?.pathPrefix ?? "/agent";
150
- const expectedServiceKey =
151
- options?.serviceKey ??
152
- readRuntimeEnv(options?.serviceKeyEnvVar ?? "AGENT_BRIDGE_SERVICE_KEY");
168
+ const configuredServiceKeys = resolveConfiguredServiceKeys({
169
+ serviceKeys: options?.serviceKeys,
170
+ serviceKeysEnvVar: options?.serviceKeysEnvVar ?? "AGENT_BRIDGE_SERVICE_KEYS_JSON",
171
+ });
153
172
  const normalizedConfig = normalizeAgentBridgeConfig(bridgeConfig);
154
173
  const availableFunctionKeys = Object.keys(normalizedConfig.functions);
155
174
 
@@ -157,8 +176,6 @@ export function registerRoutes(
157
176
  path: `${prefix}/execute`,
158
177
  method: "POST",
159
178
  handler: httpActionGeneric(async (ctx, request) => {
160
- const apiKey = request.headers.get("X-Agent-API-Key");
161
-
162
179
  let body: ExecuteRequestBody;
163
180
  try {
164
181
  body = await request.json();
@@ -185,22 +202,22 @@ export function registerRoutes(
185
202
  );
186
203
  }
187
204
 
188
- const authResult = apiKey
189
- ? await ctx.runMutation(component.gateway.authorizeRequest, {
190
- apiKey,
191
- functionKey,
192
- estimatedCost: body.estimatedCost,
193
- })
194
- : await authorizeWithServiceHeaders({
195
- request,
196
- expectedServiceKey,
197
- authorizeByAppKey: (appKey) =>
198
- ctx.runMutation(component.gateway.authorizeByAppKey, {
199
- appKey,
200
- functionKey,
201
- estimatedCost: body.estimatedCost,
202
- }),
203
- });
205
+ const headerValidation = validateStrictServiceHeaders({
206
+ request,
207
+ configuredServiceKeys,
208
+ });
209
+ if (!headerValidation.valid) {
210
+ return jsonResponse(
211
+ { success: false, error: headerValidation.error },
212
+ headerValidation.statusCode,
213
+ );
214
+ }
215
+
216
+ const authResult = await ctx.runMutation(component.gateway.authorizeByAppKey, {
217
+ appKey: headerValidation.appKey,
218
+ functionKey,
219
+ estimatedCost: body.estimatedCost,
220
+ });
204
221
 
205
222
  if (!authResult.authorized) {
206
223
  const response = jsonResponse(
@@ -239,6 +256,7 @@ export function registerRoutes(
239
256
 
240
257
  await ctx.runMutation(component.gateway.logAccess, {
241
258
  agentId: authResult.agentId as never,
259
+ serviceId: headerValidation.serviceId,
242
260
  functionKey,
243
261
  args,
244
262
  result,
@@ -255,6 +273,7 @@ export function registerRoutes(
255
273
 
256
274
  await ctx.runMutation(component.gateway.logAccess, {
257
275
  agentId: authResult.agentId as never,
276
+ serviceId: headerValidation.serviceId,
258
277
  functionKey,
259
278
  args: body.args ?? {},
260
279
  error: errorMessage,
@@ -346,54 +365,71 @@ function jsonResponse(data: unknown, status: number): Response {
346
365
  });
347
366
  }
348
367
 
349
- async function authorizeWithServiceHeaders(args: {
368
+ function validateStrictServiceHeaders(args: {
350
369
  request: Request;
351
- expectedServiceKey?: string;
352
- authorizeByAppKey: (
353
- appKey: string,
354
- ) => Promise<
355
- | { authorized: true; agentId: string }
356
- | {
357
- authorized: false;
358
- error: string;
359
- statusCode: number;
360
- agentId?: string;
361
- retryAfterSeconds?: number;
362
- }
363
- >;
364
- }) {
365
- const providedServiceKey = args.request.headers.get("X-Agent-Service-Key");
366
- const appKey = args.request.headers.get("X-Agent-App");
370
+ configuredServiceKeys:
371
+ | { ok: true; keysByServiceId: Record<string, string> }
372
+ | { ok: false; error: string };
373
+ }):
374
+ | { valid: true; serviceId: string; appKey: string }
375
+ | { valid: false; error: string; statusCode: number } {
376
+ const serviceId = args.request.headers.get("X-Agent-Service-Id")?.trim();
377
+ const providedServiceKey = args.request.headers
378
+ .get("X-Agent-Service-Key")
379
+ ?.trim();
380
+ const appKey = args.request.headers.get("X-Agent-App")?.trim();
381
+
382
+ if (!serviceId) {
383
+ return {
384
+ valid: false,
385
+ error: "Missing required header: X-Agent-Service-Id",
386
+ statusCode: 400,
387
+ };
388
+ }
367
389
  if (!providedServiceKey) {
368
390
  return {
369
- authorized: false as const,
370
- error: "Missing authentication header: X-Agent-Service-Key",
371
- statusCode: 401,
391
+ valid: false,
392
+ error: "Missing required header: X-Agent-Service-Key",
393
+ statusCode: 400,
372
394
  };
373
395
  }
374
396
  if (!appKey) {
375
397
  return {
376
- authorized: false as const,
377
- error: "Missing routing header: X-Agent-App",
398
+ valid: false,
399
+ error: "Missing required header: X-Agent-App",
378
400
  statusCode: 400,
379
401
  };
380
402
  }
381
- if (!args.expectedServiceKey) {
403
+
404
+ if (!args.configuredServiceKeys.ok) {
382
405
  return {
383
- authorized: false as const,
384
- error:
385
- "Bridge service key is not configured. Set AGENT_BRIDGE_SERVICE_KEY or pass registerRoutes({ serviceKey })",
406
+ valid: false,
407
+ error: args.configuredServiceKeys.error,
386
408
  statusCode: 500,
387
409
  };
388
410
  }
389
- if (providedServiceKey !== args.expectedServiceKey) {
411
+
412
+ const expectedServiceKey = args.configuredServiceKeys.keysByServiceId[serviceId];
413
+ if (!expectedServiceKey) {
414
+ return {
415
+ valid: false,
416
+ error: `Unknown service id: ${serviceId}`,
417
+ statusCode: 401,
418
+ };
419
+ }
420
+ if (providedServiceKey !== expectedServiceKey) {
390
421
  return {
391
- authorized: false as const,
422
+ valid: false,
392
423
  error: "Invalid service key",
393
424
  statusCode: 401,
394
425
  };
395
426
  }
396
- return await args.authorizeByAppKey(appKey);
427
+
428
+ return {
429
+ valid: true,
430
+ serviceId,
431
+ appKey,
432
+ };
397
433
  }
398
434
 
399
435
  function readRuntimeEnv(name: string): string | undefined {
@@ -406,3 +442,73 @@ function readRuntimeEnv(name: string): string | undefined {
406
442
  const trimmed = value.trim();
407
443
  return trimmed.length > 0 ? trimmed : undefined;
408
444
  }
445
+
446
+ function resolveConfiguredServiceKeys(args: {
447
+ serviceKeys?: Record<string, string>;
448
+ serviceKeysEnvVar: string;
449
+ }):
450
+ | { ok: true; keysByServiceId: Record<string, string> }
451
+ | { ok: false; error: string } {
452
+ if (args.serviceKeys) {
453
+ return sanitizeServiceKeysMap(args.serviceKeys);
454
+ }
455
+
456
+ const json = readRuntimeEnv(args.serviceKeysEnvVar);
457
+ if (!json) {
458
+ return {
459
+ ok: false,
460
+ error: `Bridge service keys are not configured. Provide registerRoutes({ serviceKeys }) or set ${args.serviceKeysEnvVar}`,
461
+ };
462
+ }
463
+
464
+ let parsed: unknown;
465
+ try {
466
+ parsed = JSON.parse(json);
467
+ } catch {
468
+ return {
469
+ ok: false,
470
+ error: `Invalid JSON in ${args.serviceKeysEnvVar}`,
471
+ };
472
+ }
473
+
474
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
475
+ return {
476
+ ok: false,
477
+ error: `${args.serviceKeysEnvVar} must be a JSON object mapping serviceId to serviceKey`,
478
+ };
479
+ }
480
+
481
+ return sanitizeServiceKeysMap(parsed as Record<string, unknown>);
482
+ }
483
+
484
+ function sanitizeServiceKeysMap(
485
+ input: Record<string, unknown>,
486
+ ): { ok: true; keysByServiceId: Record<string, string> } | { ok: false; error: string } {
487
+ const keysByServiceId: Record<string, string> = {};
488
+ for (const [serviceIdRaw, serviceKeyRaw] of Object.entries(input)) {
489
+ if (typeof serviceKeyRaw !== "string") {
490
+ return {
491
+ ok: false,
492
+ error: `Invalid service key value for "${serviceIdRaw}"`,
493
+ };
494
+ }
495
+ const serviceId = serviceIdRaw.trim();
496
+ const serviceKey = serviceKeyRaw.trim();
497
+ if (!serviceId || !serviceKey) {
498
+ return {
499
+ ok: false,
500
+ error: "Service ids and service keys cannot be empty",
501
+ };
502
+ }
503
+ keysByServiceId[serviceId] = serviceKey;
504
+ }
505
+
506
+ if (Object.keys(keysByServiceId).length === 0) {
507
+ return {
508
+ ok: false,
509
+ error: "At least one service key must be configured",
510
+ };
511
+ }
512
+
513
+ return { ok: true, keysByServiceId };
514
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ buildAgentBridgeStrictHeaders,
4
+ createAuth0TokenAdapter,
5
+ createCustomOidcTokenAdapter,
6
+ createNextAuthConvexTokenAdapter,
7
+ decodeJwtClaims,
8
+ resolveUserToken,
9
+ validateJwtClaims,
10
+ } from "./userAuth.js";
11
+
12
+ const TEST_JWT =
13
+ "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vZGVtby5jb252ZXguc2l0ZSIsImF1ZCI6ImNvbnZleCIsImV4cCI6NDA3MDkwODgwMH0.";
14
+
15
+ describe("user auth helpers", () => {
16
+ test("builds strict headers and includes bearer when provided", () => {
17
+ const headers = buildAgentBridgeStrictHeaders({
18
+ serviceId: "openclaw-prod",
19
+ serviceKey: "abs_live_example",
20
+ appKey: "crm",
21
+ userToken: "jwt_token",
22
+ });
23
+
24
+ expect(headers["X-Agent-Service-Id"]).toBe("openclaw-prod");
25
+ expect(headers["X-Agent-Service-Key"]).toBe("abs_live_example");
26
+ expect(headers["X-Agent-App"]).toBe("crm");
27
+ expect(headers.Authorization).toBe("Bearer jwt_token");
28
+ });
29
+
30
+ test("decodes jwt claims", () => {
31
+ const claims = decodeJwtClaims(TEST_JWT);
32
+ expect(claims?.sub).toBe("user_123");
33
+ expect(claims?.iss).toBe("https://demo.convex.site");
34
+ expect(claims?.aud).toBe("convex");
35
+ });
36
+
37
+ test("validates jwt claims with issuer and audience", () => {
38
+ const validation = validateJwtClaims(TEST_JWT, {
39
+ expectedIssuer: "https://demo.convex.site",
40
+ expectedAudience: "convex",
41
+ nowMs: Date.UTC(2026, 0, 1),
42
+ });
43
+ expect(validation.valid).toBe(true);
44
+ });
45
+
46
+ test("fails when jwt is expired", () => {
47
+ const expired = validateJwtClaims(TEST_JWT, {
48
+ nowMs: Date.UTC(2100, 0, 1),
49
+ });
50
+ expect(expired.valid).toBe(false);
51
+ expect(expired.reason).toBe("expired");
52
+ });
53
+
54
+ test("resolves nextauth convex token", async () => {
55
+ const adapter = createNextAuthConvexTokenAdapter({
56
+ getSession: async () => ({ convexToken: "convex_jwt" }),
57
+ });
58
+ const token = await resolveUserToken(adapter);
59
+ expect(token).toBe("convex_jwt");
60
+ });
61
+
62
+ test("resolves auth0 token", async () => {
63
+ const adapter = createAuth0TokenAdapter({
64
+ getAccessToken: async () => "auth0_jwt",
65
+ });
66
+ const token = await resolveUserToken(adapter);
67
+ expect(token).toBe("auth0_jwt");
68
+ });
69
+
70
+ test("resolves custom oidc token", async () => {
71
+ const adapter = createCustomOidcTokenAdapter({
72
+ getToken: async () => "oidc_jwt",
73
+ });
74
+ const token = await resolveUserToken(adapter);
75
+ expect(token).toBe("oidc_jwt");
76
+ });
77
+ });
@@ -0,0 +1,192 @@
1
+ export type TokenSource = "nextauth_convex" | "auth0" | "custom_oidc";
2
+
3
+ export type JwtAudience = string | Array<string>;
4
+
5
+ export interface JwtClaims {
6
+ sub?: string;
7
+ iss?: string;
8
+ aud?: JwtAudience;
9
+ exp?: number;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export interface JwtClaimValidationOptions {
14
+ expectedIssuer?: string;
15
+ expectedAudience?: string;
16
+ nowMs?: number;
17
+ }
18
+
19
+ export interface JwtClaimValidationResult {
20
+ valid: boolean;
21
+ reason?: "malformed_token" | "expired" | "issuer_mismatch" | "audience_mismatch";
22
+ claims: JwtClaims | null;
23
+ }
24
+
25
+ export interface AgentBridgeStrictHeadersInput {
26
+ serviceId: string;
27
+ serviceKey: string;
28
+ appKey: string;
29
+ userToken?: string | null;
30
+ }
31
+
32
+ export interface NextAuthSessionLike {
33
+ convexToken?: string | null;
34
+ }
35
+
36
+ export type UserTokenResolver = () => Promise<string | null>;
37
+
38
+ export interface TokenSourceAdapter {
39
+ tokenSource: TokenSource;
40
+ resolveUserToken: UserTokenResolver;
41
+ }
42
+
43
+ export function buildAgentBridgeStrictHeaders(
44
+ input: AgentBridgeStrictHeadersInput,
45
+ ): Record<string, string> {
46
+ const headers: Record<string, string> = {
47
+ "Content-Type": "application/json",
48
+ "X-Agent-Service-Id": input.serviceId,
49
+ "X-Agent-Service-Key": input.serviceKey,
50
+ "X-Agent-App": input.appKey,
51
+ };
52
+
53
+ if (input.userToken) {
54
+ headers.Authorization = `Bearer ${input.userToken}`;
55
+ }
56
+
57
+ return headers;
58
+ }
59
+
60
+ export function decodeJwtClaims(token: string): JwtClaims | null {
61
+ const parts = token.split(".");
62
+ if (parts.length !== 3 || !parts[1]) {
63
+ return null;
64
+ }
65
+
66
+ try {
67
+ const payload = decodeBase64Url(parts[1]);
68
+ const claims = JSON.parse(payload) as JwtClaims;
69
+ return claims;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function validateJwtClaims(
76
+ token: string,
77
+ options?: JwtClaimValidationOptions,
78
+ ): JwtClaimValidationResult {
79
+ const claims = decodeJwtClaims(token);
80
+ if (!claims) {
81
+ return {
82
+ valid: false,
83
+ reason: "malformed_token",
84
+ claims: null,
85
+ };
86
+ }
87
+
88
+ const nowMs = options?.nowMs ?? Date.now();
89
+ if (typeof claims.exp === "number") {
90
+ const expiresAtMs = claims.exp * 1000;
91
+ if (expiresAtMs <= nowMs) {
92
+ return {
93
+ valid: false,
94
+ reason: "expired",
95
+ claims,
96
+ };
97
+ }
98
+ }
99
+
100
+ if (options?.expectedIssuer && claims.iss !== options.expectedIssuer) {
101
+ return {
102
+ valid: false,
103
+ reason: "issuer_mismatch",
104
+ claims,
105
+ };
106
+ }
107
+
108
+ if (options?.expectedAudience) {
109
+ const audience = claims.aud;
110
+ const hasAudienceMatch = Array.isArray(audience)
111
+ ? audience.includes(options.expectedAudience)
112
+ : audience === options.expectedAudience;
113
+
114
+ if (!hasAudienceMatch) {
115
+ return {
116
+ valid: false,
117
+ reason: "audience_mismatch",
118
+ claims,
119
+ };
120
+ }
121
+ }
122
+
123
+ return {
124
+ valid: true,
125
+ claims,
126
+ };
127
+ }
128
+
129
+ export function createNextAuthConvexTokenAdapter(args: {
130
+ getSession: () => Promise<NextAuthSessionLike | null | undefined>;
131
+ }): TokenSourceAdapter {
132
+ return {
133
+ tokenSource: "nextauth_convex",
134
+ resolveUserToken: async () => {
135
+ const session = await args.getSession();
136
+ const token = session?.convexToken;
137
+ if (typeof token !== "string" || token.trim().length === 0) {
138
+ return null;
139
+ }
140
+ return token;
141
+ },
142
+ };
143
+ }
144
+
145
+ export function createAuth0TokenAdapter(args: {
146
+ getAccessToken: () => Promise<string | null | undefined>;
147
+ }): TokenSourceAdapter {
148
+ return {
149
+ tokenSource: "auth0",
150
+ resolveUserToken: async () => {
151
+ const token = await args.getAccessToken();
152
+ if (typeof token !== "string" || token.trim().length === 0) {
153
+ return null;
154
+ }
155
+ return token;
156
+ },
157
+ };
158
+ }
159
+
160
+ export function createCustomOidcTokenAdapter(args: {
161
+ getToken: () => Promise<string | null | undefined>;
162
+ }): TokenSourceAdapter {
163
+ return {
164
+ tokenSource: "custom_oidc",
165
+ resolveUserToken: async () => {
166
+ const token = await args.getToken();
167
+ if (typeof token !== "string" || token.trim().length === 0) {
168
+ return null;
169
+ }
170
+ return token;
171
+ },
172
+ };
173
+ }
174
+
175
+ export async function resolveUserToken(
176
+ adapter: TokenSourceAdapter,
177
+ ): Promise<string | null> {
178
+ return await adapter.resolveUserToken();
179
+ }
180
+
181
+ function decodeBase64Url(value: string): string {
182
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
183
+ const paddingLength = (4 - (normalized.length % 4)) % 4;
184
+ const padded = normalized + "=".repeat(paddingLength);
185
+
186
+ if (typeof atob === "function") {
187
+ return atob(padded);
188
+ }
189
+
190
+ const buffer = Buffer.from(padded, "base64");
191
+ return buffer.toString("utf-8");
192
+ }
@@ -112,6 +112,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
112
112
  error?: string;
113
113
  functionKey: string;
114
114
  result?: any;
115
+ serviceId?: string;
115
116
  timestamp: number;
116
117
  },
117
118
  null,
@@ -120,7 +121,12 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
120
121
  queryAccessLog: FunctionReference<
121
122
  "query",
122
123
  "internal",
123
- { agentId?: string; functionKey?: string; limit?: number },
124
+ {
125
+ agentId?: string;
126
+ functionKey?: string;
127
+ limit?: number;
128
+ serviceId?: string;
129
+ },
124
130
  Array<{
125
131
  _id: string;
126
132
  agentId: string;
@@ -129,6 +135,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
129
135
  error?: string;
130
136
  functionKey: string;
131
137
  result?: any;
138
+ serviceId?: string;
132
139
  timestamp: number;
133
140
  }>,
134
141
  Name
@@ -100,6 +100,7 @@ export const authorizeByAppKey = mutation({
100
100
  export const logAccess = mutation({
101
101
  args: {
102
102
  agentId: v.id("agents"),
103
+ serviceId: v.optional(v.string()),
103
104
  functionKey: v.string(),
104
105
  args: v.any(),
105
106
  result: v.optional(v.any()),
@@ -112,6 +113,7 @@ export const logAccess = mutation({
112
113
  await ctx.db.insert("agentLogs", {
113
114
  timestamp: args.timestamp,
114
115
  agentId: args.agentId,
116
+ serviceId: args.serviceId,
115
117
  functionKey: args.functionKey,
116
118
  args: args.args,
117
119
  result: args.result,
@@ -128,6 +130,7 @@ export const logAccess = mutation({
128
130
  export const queryAccessLog = query({
129
131
  args: {
130
132
  agentId: v.optional(v.id("agents")),
133
+ serviceId: v.optional(v.string()),
131
134
  functionKey: v.optional(v.string()),
132
135
  limit: v.optional(v.number()),
133
136
  },
@@ -136,6 +139,7 @@ export const queryAccessLog = query({
136
139
  _id: v.id("agentLogs"),
137
140
  timestamp: v.number(),
138
141
  agentId: v.id("agents"),
142
+ serviceId: v.optional(v.string()),
139
143
  functionKey: v.string(),
140
144
  args: v.any(),
141
145
  result: v.optional(v.any()),
@@ -146,6 +150,33 @@ export const queryAccessLog = query({
146
150
  handler: async (ctx, args) => {
147
151
  const limit = args.limit ?? 50;
148
152
 
153
+ if (args.serviceId !== undefined) {
154
+ const logs = await ctx.db
155
+ .query("agentLogs")
156
+ .withIndex("by_serviceId_and_timestamp", (q) =>
157
+ q.eq("serviceId", args.serviceId),
158
+ )
159
+ .order("desc")
160
+ .take(limit);
161
+
162
+ const filteredLogs =
163
+ args.functionKey !== undefined
164
+ ? logs.filter((log) => log.functionKey === args.functionKey)
165
+ : logs;
166
+
167
+ return filteredLogs.map((l) => ({
168
+ _id: l._id,
169
+ timestamp: l.timestamp,
170
+ agentId: l.agentId,
171
+ serviceId: l.serviceId,
172
+ functionKey: l.functionKey,
173
+ args: l.args,
174
+ result: l.result,
175
+ error: l.error,
176
+ duration: l.duration,
177
+ }));
178
+ }
179
+
149
180
  const agentId = args.agentId;
150
181
  if (agentId !== undefined) {
151
182
  const logs = await ctx.db
@@ -160,6 +191,7 @@ export const queryAccessLog = query({
160
191
  _id: l._id,
161
192
  timestamp: l.timestamp,
162
193
  agentId: l.agentId,
194
+ serviceId: l.serviceId,
163
195
  functionKey: l.functionKey,
164
196
  args: l.args,
165
197
  result: l.result,
@@ -181,6 +213,7 @@ export const queryAccessLog = query({
181
213
  _id: l._id,
182
214
  timestamp: l.timestamp,
183
215
  agentId: l.agentId,
216
+ serviceId: l.serviceId,
184
217
  functionKey: l.functionKey,
185
218
  args: l.args,
186
219
  result: l.result,
@@ -40,6 +40,7 @@ export default defineSchema({
40
40
 
41
41
  agentLogs: defineTable({
42
42
  agentId: v.id("agents"),
43
+ serviceId: v.optional(v.string()),
43
44
  functionKey: v.string(),
44
45
  args: v.any(),
45
46
  result: v.optional(v.any()),
@@ -48,6 +49,7 @@ export default defineSchema({
48
49
  timestamp: v.number(),
49
50
  })
50
51
  .index("by_agentId_and_timestamp", ["agentId", "timestamp"])
52
+ .index("by_serviceId_and_timestamp", ["serviceId", "timestamp"])
51
53
  .index("by_functionKey", ["functionKey"])
52
54
  .index("by_timestamp", ["timestamp"]),
53
55
  });