@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1442.6 → 1.0.0-OCP-1449.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.
Files changed (64) hide show
  1. package/README.md +114 -72
  2. package/dist/auth/AuthUtils.d.ts +12 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +80 -25
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/auth/AuthUtils.test.js +161 -117
  7. package/dist/auth/AuthUtils.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +1 -1
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +17 -4
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +54 -8
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +1 -1
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +24 -4
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +260 -8
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/ToolLogger.d.ts.map +1 -1
  25. package/dist/logging/ToolLogger.js +2 -1
  26. package/dist/logging/ToolLogger.js.map +1 -1
  27. package/dist/logging/ToolLogger.test.js +114 -2
  28. package/dist/logging/ToolLogger.test.js.map +1 -1
  29. package/dist/service/Service.d.ts +88 -2
  30. package/dist/service/Service.d.ts.map +1 -1
  31. package/dist/service/Service.js +227 -55
  32. package/dist/service/Service.js.map +1 -1
  33. package/dist/service/Service.test.js +464 -36
  34. package/dist/service/Service.test.js.map +1 -1
  35. package/dist/types/ToolError.d.ts +59 -0
  36. package/dist/types/ToolError.d.ts.map +1 -0
  37. package/dist/types/ToolError.js +79 -0
  38. package/dist/types/ToolError.js.map +1 -0
  39. package/dist/types/ToolError.test.d.ts +2 -0
  40. package/dist/types/ToolError.test.d.ts.map +1 -0
  41. package/dist/types/ToolError.test.js +161 -0
  42. package/dist/types/ToolError.test.js.map +1 -0
  43. package/dist/validation/ParameterValidator.d.ts +5 -16
  44. package/dist/validation/ParameterValidator.d.ts.map +1 -1
  45. package/dist/validation/ParameterValidator.js +10 -3
  46. package/dist/validation/ParameterValidator.js.map +1 -1
  47. package/dist/validation/ParameterValidator.test.js +186 -146
  48. package/dist/validation/ParameterValidator.test.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/auth/AuthUtils.test.ts +176 -157
  51. package/src/auth/AuthUtils.ts +96 -33
  52. package/src/function/GlobalToolFunction.test.ts +54 -8
  53. package/src/function/GlobalToolFunction.ts +26 -6
  54. package/src/function/ToolFunction.test.ts +274 -8
  55. package/src/function/ToolFunction.ts +33 -7
  56. package/src/index.ts +1 -0
  57. package/src/logging/ToolLogger.test.ts +118 -2
  58. package/src/logging/ToolLogger.ts +2 -1
  59. package/src/service/Service.test.ts +577 -34
  60. package/src/service/Service.ts +286 -54
  61. package/src/types/ToolError.test.ts +192 -0
  62. package/src/types/ToolError.ts +95 -0
  63. package/src/validation/ParameterValidator.test.ts +185 -158
  64. package/src/validation/ParameterValidator.ts +17 -20
@@ -22,12 +22,22 @@ jest.mock('@zaiusinc/app-sdk', () => ({
22
22
  }
23
23
  },
24
24
  Request: jest.fn().mockImplementation(() => ({})),
25
- Response: jest.fn().mockImplementation((status, data) => ({
25
+ Response: jest.fn().mockImplementation((status, data, headers) => ({
26
26
  status,
27
27
  data,
28
28
  bodyJSON: data,
29
- bodyAsU8Array: new Uint8Array()
29
+ bodyAsU8Array: new Uint8Array(),
30
+ headers
30
31
  })),
32
+ Headers: jest.fn().mockImplementation((entries) => {
33
+ const headers = new Map();
34
+ if (entries) {
35
+ for (const [key, value] of entries) {
36
+ headers.set(key, value);
37
+ }
38
+ }
39
+ return headers;
40
+ }),
31
41
  amendLogContext: jest.fn(),
32
42
  getAppContext: jest.fn(),
33
43
  logger: {
@@ -251,7 +261,13 @@ describe('ToolFunction', () => {
251
261
 
252
262
  const result = await toolFunction.perform();
253
263
 
254
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
264
+ expect(result.status).toBe(403);
265
+ expect(result.bodyJSON).toEqual({
266
+ title: 'Forbidden',
267
+ status: 403,
268
+ detail: 'Invalid OptiID access token',
269
+ instance: '/test'
270
+ });
255
271
  expect(mockGetTokenVerifier).toHaveBeenCalled();
256
272
  expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
257
273
  expect(mockProcessRequest).not.toHaveBeenCalled();
@@ -277,7 +293,13 @@ describe('ToolFunction', () => {
277
293
 
278
294
  const result = await toolFunctionWithDifferentOrgId.perform();
279
295
 
280
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
296
+ expect(result.status).toBe(403);
297
+ expect(result.bodyJSON).toEqual({
298
+ title: 'Forbidden',
299
+ status: 403,
300
+ detail: 'Organization ID does not match',
301
+ instance: '/test'
302
+ });
281
303
  expect(mockGetAppContext).toHaveBeenCalled();
282
304
  expect(mockProcessRequest).not.toHaveBeenCalled();
283
305
  });
@@ -302,7 +324,13 @@ describe('ToolFunction', () => {
302
324
 
303
325
  const result = await toolFunctionWithoutToken.perform();
304
326
 
305
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
327
+ expect(result.status).toBe(403);
328
+ expect(result.bodyJSON).toEqual({
329
+ title: 'Forbidden',
330
+ status: 403,
331
+ detail: 'OptiID access token is required',
332
+ instance: '/test'
333
+ });
306
334
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
307
335
  expect(mockProcessRequest).not.toHaveBeenCalled();
308
336
  });
@@ -327,7 +355,13 @@ describe('ToolFunction', () => {
327
355
 
328
356
  const result = await toolFunctionWithoutCustomerId.perform();
329
357
 
330
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
358
+ expect(result.status).toBe(403);
359
+ expect(result.bodyJSON).toEqual({
360
+ title: 'Forbidden',
361
+ status: 403,
362
+ detail: 'Organization ID is required',
363
+ instance: '/test'
364
+ });
331
365
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
332
366
  expect(mockProcessRequest).not.toHaveBeenCalled();
333
367
  });
@@ -346,7 +380,13 @@ describe('ToolFunction', () => {
346
380
 
347
381
  const result = await toolFunctionWithoutAuth.perform();
348
382
 
349
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
383
+ expect(result.status).toBe(403);
384
+ expect(result.bodyJSON).toEqual({
385
+ title: 'Forbidden',
386
+ status: 403,
387
+ detail: 'Authentication data is required',
388
+ instance: '/test'
389
+ });
350
390
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
351
391
  expect(mockProcessRequest).not.toHaveBeenCalled();
352
392
  });
@@ -357,7 +397,13 @@ describe('ToolFunction', () => {
357
397
 
358
398
  const result = await toolFunction.perform();
359
399
 
360
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
400
+ expect(result.status).toBe(403);
401
+ expect(result.bodyJSON).toEqual({
402
+ title: 'Forbidden',
403
+ status: 403,
404
+ detail: 'Token verification failed',
405
+ instance: '/test'
406
+ });
361
407
  expect(mockGetTokenVerifier).toHaveBeenCalled();
362
408
  expect(mockProcessRequest).not.toHaveBeenCalled();
363
409
  });
@@ -374,4 +420,224 @@ describe('ToolFunction', () => {
374
420
  expect(toolFunction.getRequest()).toBe(mockRequest);
375
421
  });
376
422
  });
423
+
424
+ describe('internal request authentication', () => {
425
+ beforeEach(() => {
426
+ // Reset mocks before each test
427
+ jest.clearAllMocks();
428
+ setupAuthMocks();
429
+ });
430
+
431
+ it('should use internal authentication for /overrides endpoint', async () => {
432
+ const overridesRequest = {
433
+ path: '/overrides',
434
+ method: 'DELETE',
435
+ bodyJSON: {},
436
+ headers: {
437
+ get: jest.fn().mockImplementation((name: string) => {
438
+ if (name === 'Authorization' || name === 'authorization') return 'internal-token';
439
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
440
+ return null;
441
+ })
442
+ }
443
+ };
444
+
445
+ mockProcessRequest.mockResolvedValue(mockResponse);
446
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
447
+ const result = await toolFunctionOverrides.perform();
448
+
449
+ expect(result).toBe(mockResponse);
450
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
451
+ });
452
+
453
+ it('should throw ToolError when internal authentication fails for /overrides endpoint', async () => {
454
+ const overridesRequest = {
455
+ path: '/overrides',
456
+ method: 'DELETE',
457
+ bodyJSON: {},
458
+ headers: {
459
+ get: jest.fn().mockImplementation((name: string) => {
460
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
461
+ return null; // No Authorization header
462
+ })
463
+ }
464
+ };
465
+
466
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
467
+ const result = await toolFunctionOverrides.perform();
468
+
469
+ expect(result.status).toBe(401);
470
+ expect(result.bodyJSON).toEqual({
471
+ title: 'Unauthorized',
472
+ status: 401,
473
+ detail: 'Internal request authentication failed',
474
+ instance: '/overrides'
475
+ });
476
+ expect(mockProcessRequest).not.toHaveBeenCalled();
477
+ });
478
+
479
+ it('should use regular authentication for non-overrides endpoints', async () => {
480
+ const regularRequest = {
481
+ ...mockRequest,
482
+ path: '/regular-tool',
483
+ headers: {
484
+ get: jest.fn().mockImplementation((name: string) => {
485
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
486
+ return null;
487
+ })
488
+ }
489
+ };
490
+
491
+ mockProcessRequest.mockResolvedValue(mockResponse);
492
+ const toolFunctionRegular = new TestToolFunction(regularRequest);
493
+ const result = await toolFunctionRegular.perform();
494
+
495
+ expect(result).toBe(mockResponse);
496
+ expect(mockProcessRequest).toHaveBeenCalledWith(regularRequest, toolFunctionRegular);
497
+ });
498
+
499
+ it('should handle internal authentication with valid Authorization header', async () => {
500
+ const overridesRequest = {
501
+ path: '/overrides',
502
+ method: 'PATCH',
503
+ bodyJSON: {
504
+ functions: [
505
+ {
506
+ name: 'test_tool',
507
+ description: 'Updated description'
508
+ }
509
+ ]
510
+ },
511
+ headers: {
512
+ get: jest.fn().mockImplementation((name: string) => {
513
+ if (name === 'Authorization') return 'valid-internal-token';
514
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
515
+ return null;
516
+ })
517
+ }
518
+ };
519
+
520
+ mockProcessRequest.mockResolvedValue(mockResponse);
521
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
522
+ const result = await toolFunctionOverrides.perform();
523
+
524
+ expect(result).toBe(mockResponse);
525
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
526
+ });
527
+
528
+ it('should handle internal authentication with lowercase authorization header', async () => {
529
+ const overridesRequest = {
530
+ path: '/overrides',
531
+ method: 'PATCH',
532
+ bodyJSON: {
533
+ functions: [
534
+ {
535
+ name: 'test_tool',
536
+ description: 'Updated description'
537
+ }
538
+ ]
539
+ },
540
+ headers: {
541
+ get: jest.fn().mockImplementation((name: string) => {
542
+ if (name === 'authorization') return 'valid-internal-token';
543
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
544
+ return null;
545
+ })
546
+ }
547
+ };
548
+
549
+ mockProcessRequest.mockResolvedValue(mockResponse);
550
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
551
+ const result = await toolFunctionOverrides.perform();
552
+
553
+ expect(result).toBe(mockResponse);
554
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
555
+ });
556
+
557
+ it('should fail internal authentication when token verification fails', async () => {
558
+ // Mock token verifier to return false for invalid token
559
+ mockTokenVerifier.verify.mockResolvedValue(false);
560
+
561
+ const overridesRequest = {
562
+ path: '/overrides',
563
+ method: 'DELETE',
564
+ bodyJSON: {},
565
+ headers: {
566
+ get: jest.fn().mockImplementation((name: string) => {
567
+ if (name === 'Authorization') return 'invalid-token';
568
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
569
+ return null;
570
+ })
571
+ }
572
+ };
573
+
574
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
575
+ const result = await toolFunctionOverrides.perform();
576
+
577
+ expect(result.status).toBe(401);
578
+ expect(result.bodyJSON).toEqual({
579
+ title: 'Unauthorized',
580
+ status: 401,
581
+ detail: 'Internal request authentication failed',
582
+ instance: '/overrides'
583
+ });
584
+ expect(mockProcessRequest).not.toHaveBeenCalled();
585
+ });
586
+
587
+ it('should fail internal authentication when token verifier throws error', async () => {
588
+ // Mock token verifier to throw an error
589
+ mockTokenVerifier.verify.mockRejectedValue(new Error('Token service unavailable'));
590
+
591
+ const overridesRequest = {
592
+ path: '/overrides',
593
+ method: 'DELETE',
594
+ bodyJSON: {},
595
+ headers: {
596
+ get: jest.fn().mockImplementation((name: string) => {
597
+ if (name === 'Authorization') return 'some-token';
598
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
599
+ return null;
600
+ })
601
+ }
602
+ };
603
+
604
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
605
+ const result = await toolFunctionOverrides.perform();
606
+
607
+ expect(result.status).toBe(401);
608
+ expect(result.bodyJSON).toEqual({
609
+ title: 'Unauthorized',
610
+ status: 401,
611
+ detail: 'Internal request authentication failed',
612
+ instance: '/overrides'
613
+ });
614
+ expect(mockProcessRequest).not.toHaveBeenCalled();
615
+ });
616
+
617
+ it('should fail internal authentication when headers object is missing', async () => {
618
+ const overridesRequest = {
619
+ path: '/overrides',
620
+ method: 'DELETE',
621
+ bodyJSON: {},
622
+ headers: {
623
+ get: jest.fn().mockImplementation((name: string) => {
624
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
625
+ return null;
626
+ })
627
+ }
628
+ };
629
+
630
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
631
+ const result = await toolFunctionOverrides.perform();
632
+
633
+ expect(result.status).toBe(401);
634
+ expect(result.bodyJSON).toEqual({
635
+ title: 'Unauthorized',
636
+ status: 401,
637
+ detail: 'Internal request authentication failed',
638
+ instance: '/overrides'
639
+ });
640
+ expect(mockProcessRequest).not.toHaveBeenCalled();
641
+ });
642
+ });
377
643
  });
@@ -1,7 +1,8 @@
1
- import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
- import { authenticateRegularRequest } from '../auth/AuthUtils';
1
+ import { Function, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
2
+ import { authenticateRegularRequest, authenticateInternalRequest } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
4
  import { ToolLogger } from '../logging/ToolLogger';
5
+ import { ToolError } from '../types/ToolError';
5
6
 
6
7
  /**
7
8
  * Abstract base class for tool-based function execution
@@ -42,8 +43,27 @@ export abstract class ToolFunction extends Function {
42
43
  * @returns Response as the HTTP response
43
44
  */
44
45
  private async handleRequest(): Promise<Response> {
45
- if (!(await this.authorizeRequest())) {
46
- return new Response(403, { error: 'Forbidden' });
46
+ try {
47
+ await this.authorizeRequest();
48
+ } catch (error) {
49
+ if (error instanceof ToolError) {
50
+ return new Response(
51
+ error.status,
52
+ error.toProblemDetails(this.request.path),
53
+ new Headers([['content-type', 'application/problem+json']])
54
+ );
55
+ }
56
+ // Fallback for unexpected errors
57
+ return new Response(
58
+ 500,
59
+ {
60
+ title: 'Internal Server Error',
61
+ status: 500,
62
+ detail: 'An unexpected error occurred during authentication',
63
+ instance: this.request.path
64
+ },
65
+ new Headers([['content-type', 'application/problem+json']])
66
+ );
47
67
  }
48
68
 
49
69
  if (this.request.path === '/ready') {
@@ -58,9 +78,15 @@ export abstract class ToolFunction extends Function {
58
78
  /**
59
79
  * Authenticate the incoming request by validating the OptiID token and organization ID
60
80
  *
61
- * @returns true if authentication succeeds
81
+ * @throws {ToolError} If authentication fails
62
82
  */
63
- private async authorizeRequest(): Promise<boolean> {
64
- return await authenticateRegularRequest(this.request);
83
+ private async authorizeRequest(): Promise<void> {
84
+ // Use internal authentication for overrides endpoint (header-based token)
85
+ if (this.request.path === '/overrides') {
86
+ await authenticateInternalRequest(this.request);
87
+ } else {
88
+ // Use regular authentication for other endpoints (body-based token with org validation)
89
+ await authenticateRegularRequest(this.request);
90
+ }
65
91
  }
66
92
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './function/ToolFunction';
2
2
  export * from './function/GlobalToolFunction';
3
3
  export * from './types/Models';
4
+ export * from './types/ToolError';
4
5
  export * from './decorator/Decorator';
5
6
  export * from './auth/TokenVerifier';
6
7
  export { Tool, Interaction, InteractionResult } from './service/Service';
@@ -32,6 +32,7 @@ describe('ToolLogger', () => {
32
32
  const createMockRequest = (overrides: any = {}): App.Request => {
33
33
  const defaultRequest = {
34
34
  path: '/test-tool',
35
+ method: 'POST',
35
36
  bodyJSON: {
36
37
  parameters: {
37
38
  name: 'test',
@@ -96,6 +97,7 @@ describe('ToolLogger', () => {
96
97
  const expectedLog = {
97
98
  event: 'opal_tool_request',
98
99
  path: '/test-tool',
100
+ method: 'POST',
99
101
  parameters: {
100
102
  name: 'test',
101
103
  value: 'data'
@@ -118,6 +120,7 @@ describe('ToolLogger', () => {
118
120
  expectJsonLog({
119
121
  event: 'opal_tool_request',
120
122
  path: '/test-tool',
123
+ method: 'POST',
121
124
  parameters: null
122
125
  });
123
126
  });
@@ -135,6 +138,7 @@ describe('ToolLogger', () => {
135
138
  expectJsonLog({
136
139
  event: 'opal_tool_request',
137
140
  path: '/test-tool',
141
+ method: 'POST',
138
142
  parameters: {
139
143
  name: 'direct',
140
144
  action: 'test'
@@ -167,6 +171,7 @@ describe('ToolLogger', () => {
167
171
  expectJsonLog({
168
172
  event: 'opal_tool_request',
169
173
  path: '/test-tool',
174
+ method: 'POST',
170
175
  parameters: {
171
176
  username: 'john',
172
177
  password: '[REDACTED]',
@@ -204,6 +209,7 @@ describe('ToolLogger', () => {
204
209
  expectJsonLog({
205
210
  event: 'opal_tool_request',
206
211
  path: '/test-tool',
212
+ method: 'POST',
207
213
  parameters: {
208
214
  PASSWORD: '[REDACTED]',
209
215
  API_KEY: '[REDACTED]',
@@ -232,6 +238,7 @@ describe('ToolLogger', () => {
232
238
  expectJsonLog({
233
239
  event: 'opal_tool_request',
234
240
  path: '/test-tool',
241
+ method: 'POST',
235
242
  parameters: {
236
243
  description: `${'a'.repeat(100)}... (truncated, 150 chars total)`,
237
244
  short_field: 'normal'
@@ -255,6 +262,7 @@ describe('ToolLogger', () => {
255
262
  expectJsonLog({
256
263
  event: 'opal_tool_request',
257
264
  path: '/test-tool',
265
+ method: 'POST',
258
266
  parameters: {
259
267
  items: [
260
268
  ...largeArray.slice(0, 10),
@@ -291,6 +299,7 @@ describe('ToolLogger', () => {
291
299
  expectJsonLog({
292
300
  event: 'opal_tool_request',
293
301
  path: '/test-tool',
302
+ method: 'POST',
294
303
  parameters: {
295
304
  user: {
296
305
  name: 'John',
@@ -328,6 +337,7 @@ describe('ToolLogger', () => {
328
337
  expectJsonLog({
329
338
  event: 'opal_tool_request',
330
339
  path: '/test-tool',
340
+ method: 'POST',
331
341
  parameters: {
332
342
  nullValue: null,
333
343
  emptyString: '',
@@ -353,6 +363,7 @@ describe('ToolLogger', () => {
353
363
  expectJsonLog({
354
364
  event: 'opal_tool_request',
355
365
  path: '/test-tool',
366
+ method: 'POST',
356
367
  parameters: {
357
368
  credentials: '[REDACTED]',
358
369
  public_list: ['item1', 'item2']
@@ -381,6 +392,7 @@ describe('ToolLogger', () => {
381
392
  expectJsonLog({
382
393
  event: 'opal_tool_request',
383
394
  path: '/test-tool',
395
+ method: 'POST',
384
396
  parameters: {
385
397
  auth: '[REDACTED]',
386
398
  public_config: {
@@ -496,7 +508,9 @@ describe('ToolLogger', () => {
496
508
  // First item: deeply nested object with inner parts replaced by placeholder
497
509
  expect(items[0]).toEqual({
498
510
  level2: {
499
- level3: '[MAX_DEPTH_EXCEEDED]'
511
+ level3: {
512
+ level4: '[MAX_DEPTH_EXCEEDED]'
513
+ }
500
514
  }
501
515
  });
502
516
 
@@ -509,7 +523,9 @@ describe('ToolLogger', () => {
509
523
  // Fourth item: another deeply nested object with inner parts replaced by placeholder
510
524
  expect(items[3]).toEqual({
511
525
  level2: {
512
- level3: '[MAX_DEPTH_EXCEEDED]'
526
+ level3: {
527
+ level4: '[MAX_DEPTH_EXCEEDED]'
528
+ }
513
529
  }
514
530
  });
515
531
  });
@@ -694,6 +710,7 @@ describe('ToolLogger', () => {
694
710
  expectJsonLog({
695
711
  event: 'opal_tool_request',
696
712
  path: '/test-tool',
713
+ method: 'POST',
697
714
  parameters: {}
698
715
  });
699
716
  });
@@ -712,6 +729,7 @@ describe('ToolLogger', () => {
712
729
  expectJsonLog({
713
730
  event: 'opal_tool_request',
714
731
  path: '/test-tool',
732
+ method: 'POST',
715
733
  parameters: {
716
734
  field: 'value'
717
735
  }
@@ -738,6 +756,7 @@ describe('ToolLogger', () => {
738
756
  expectJsonLog({
739
757
  event: 'opal_tool_request',
740
758
  path: '/test-tool',
759
+ method: 'POST',
741
760
  parameters: {
742
761
  string: 'text',
743
762
  number: 42,
@@ -749,5 +768,102 @@ describe('ToolLogger', () => {
749
768
  }
750
769
  });
751
770
  });
771
+
772
+ it('should handle tool override request with enhanced parameter descriptions', () => {
773
+ const overrideRequest = {
774
+ tools: [
775
+ {
776
+ name: 'calculate_experiment_runtime',
777
+ description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
778
+ parameters: [
779
+ {
780
+ name: 'BCR',
781
+ type: 'number',
782
+ description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
783
+ required: true
784
+ },
785
+ {
786
+ name: 'MDE',
787
+ type: 'number',
788
+ description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
789
+ required: true
790
+ },
791
+ {
792
+ name: 'sigLevel',
793
+ type: 'number',
794
+ description: 'OVERRIDDEN: Enhanced statistical significance level',
795
+ required: true
796
+ },
797
+ {
798
+ name: 'numVariations',
799
+ type: 'number',
800
+ description: 'OVERRIDDEN: Enhanced number of variations handling',
801
+ required: true
802
+ },
803
+ {
804
+ name: 'dailyVisitors',
805
+ type: 'number',
806
+ description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
807
+ required: true
808
+ }
809
+ ]
810
+ }
811
+ // Note: NOT including calculate_sample_size in override
812
+ ]
813
+ };
814
+
815
+ const req = createMockRequest({
816
+ path: '/overrides',
817
+ bodyJSON: overrideRequest
818
+ });
819
+
820
+ ToolLogger.logRequest(req);
821
+
822
+ expectJsonLog({
823
+ event: 'opal_tool_request',
824
+ path: '/overrides',
825
+ method: 'POST',
826
+ parameters: {
827
+ tools: [
828
+ {
829
+ name: 'calculate_experiment_runtime',
830
+ description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
831
+ parameters: [
832
+ {
833
+ name: 'BCR',
834
+ type: 'number',
835
+ description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
836
+ required: true
837
+ },
838
+ {
839
+ name: 'MDE',
840
+ type: 'number',
841
+ description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
842
+ required: true
843
+ },
844
+ {
845
+ name: 'sigLevel',
846
+ type: 'number',
847
+ description: 'OVERRIDDEN: Enhanced statistical significance level',
848
+ required: true
849
+ },
850
+ {
851
+ name: 'numVariations',
852
+ type: 'number',
853
+ description: 'OVERRIDDEN: Enhanced number of variations handling',
854
+ required: true
855
+ },
856
+ {
857
+ name: 'dailyVisitors',
858
+ type: 'number',
859
+ description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
860
+ required: true
861
+ }
862
+ ]
863
+ }
864
+ ]
865
+ }
866
+ });
867
+ });
752
868
  });
753
869
  });
@@ -75,7 +75,7 @@ export class ToolLogger {
75
75
 
76
76
  if (Array.isArray(data)) {
77
77
  const truncated = data.slice(0, this.MAX_ARRAY_ITEMS);
78
- const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth - 1));
78
+ const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth));
79
79
  if (data.length > this.MAX_ARRAY_ITEMS) {
80
80
  result.push(`... (${data.length - this.MAX_ARRAY_ITEMS} more items truncated)`);
81
81
  }
@@ -146,6 +146,7 @@ export class ToolLogger {
146
146
  const requestLog = {
147
147
  event: 'opal_tool_request',
148
148
  path: req.path,
149
+ method: req.method,
149
150
  parameters: this.createParameterSummary(params)
150
151
  };
151
152