Skip to content

Commit 0fff69c

Browse files
amikofalvyclaude
andauthored
Centralize jmes logic (#1582)
* [US-001] Create jmespath-utils module with core validation functions Add packages/agents-core/src/utils/jmespath-utils.ts with: - ValidationResult interface - MAX_EXPRESSION_LENGTH constant (1000) - validateJMESPath() for syntax validation - validateRegex() for regex pattern validation - compileJMESPath() wrapper with proper typing - searchJMESPath<T>() safe search wrapper - JMESPathExtended interface for missing compile method types Co-Authored-By: Claude Opus 4.5 <[email protected]> * [US-002] Add security validation with dangerous pattern detection - Export DANGEROUS_PATTERNS array with 6 patterns for injection prevention - Export SecurityOptions interface with maxLength and dangerousPatterns - Export validateJMESPathSecure() with ordered checks: length, patterns, compile - Validation checks follow O(1) -> O(n) -> expensive ordering for efficiency Co-Authored-By: Claude Opus 4.5 <[email protected]> * [US-003] Add jmespathString Zod factory for OpenAPI compatibility - Import z from '@hono/zod-openapi' - Export JMESPathStringOptions interface with maxLength option - Export jmespathString() function that returns z.string().max().describe() - Description includes valid/invalid JMESPath examples for OpenAPI docs Co-Authored-By: Claude Opus 4.5 <[email protected]> * [US-004] Update signature-validation to re-export from jmespath-utils - Remove original validateJMESPath and validateRegex implementations - Import and re-export both functions from jmespath-utils - Re-export ValidationResult type for backward compatibility - Public export path @inkeep/agents-core/utils/signature-validation preserved * [US-005] Refactor JsonTransformer to use shared jmespath-utils - Remove private validateJMESPath method, MAX_EXPRESSION_LENGTH constant, DANGEROUS_PATTERNS constant, and JMESPathExtended interface - Import validateJMESPathSecure, compileJMESPath, DANGEROUS_PATTERNS, and MAX_EXPRESSION_LENGTH from ./jmespath-utils - Update transform() method to use shared validation - Update objectToJMESPath() to use compileJMESPath() - Maintain backward compatibility with existing error message format * [US-006] Update trigger-auth.ts to use searchJMESPath from shared jmespath-utils - Replace direct jmespath import with searchJMESPath from './jmespath-utils' - Update extractSignature() to use searchJMESPath<string | undefined>() - Update extractComponent() to use searchJMESPath<string | undefined>() - Removes direct jmespath dependency in this file * [US-007] Update TemplateEngine.ts to use searchJMESPath from shared jmespath-utils * [US-008] Update Zod schemas to use shared jmespath-utils - Remove JMESPathExtended interface and jmespathExt constant - Import jmespathString, validateJMESPathSecure, validateRegex from jmespath-utils - Replace z.string().optional() in TriggerOutputTransformSchema with jmespathString().optional() - Update TriggerInsertSchema.superRefine() to use validateJMESPathSecure() and validateRegex() - Remove direct jmespath import * [US-009] Update trigger-form to use shared validation from agents-core - Import validateJMESPath and validateRegex from @inkeep/agents-core/utils/signature-validation - Add adapter functions to convert ValidationResult to string | undefined for form validation - Remove duplicate validateJMESPath function (lines 412-425) - Remove duplicate validateRegex function (lines 427-436) Co-Authored-By: Claude Opus 4.5 <[email protected]> * [US-010] Add comprehensive jmespath-utils test file - Add tests for validateJMESPath: valid expressions, invalid syntax errors - Add tests for validateRegex: valid patterns, invalid patterns - Add tests for validateJMESPathSecure: length validation, all 6 dangerous patterns, custom options - Add tests for searchJMESPath: successful search, type inference - Add tests for compileJMESPath: valid compile, invalid syntax throws - Add tests for jmespathString(): returns schema with maxLength, description contains examples - Add tests for MAX_EXPRESSION_LENGTH constant and ValidationResult type Co-Authored-By: Claude Opus 4.5 <[email protected]> * [US-011] Clean up duplicate tests and verify exports - Remove signature-validation.test.ts (duplicate tests now in jmespath-utils.test.ts) - Verified @inkeep/agents-core/utils/signature-validation export continues to work - Evaluated jmespath-utils export path: not needed since signature-validation re-exports for external use - Typecheck passes Co-Authored-By: Claude Opus 4.5 <[email protected]> * format fixes and openapi snapshot update * lint ignores * extracting jmes logic to jmes utils file * jmes changesets --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent caefccc commit 0fff69c

File tree

13 files changed

+848
-553
lines changed

13 files changed

+848
-553
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-core": patch
3+
---
4+
5+
Centralized jmes validation

.changeset/mobile-pink-parakeet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-manage-ui": patch
3+
---
4+
5+
Use centralized jmes validation
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-api": patch
3+
---
4+
5+
Updated openapi snapshot

agents-api/__snapshots__/openapi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8547,7 +8547,8 @@
85478547
"nullable": true,
85488548
"properties": {
85498549
"jmespath": {
8550-
"description": "JMESPath expression for payload transformation",
8550+
"description": "JMESPath expression (max 1000 chars). Valid: \"data.items[0].name\", \"results[?status=='active']\", \"keys(@)\". Invalid: \"${...}\" (template injection), \"eval(...)\", \"constructor\", \"__proto__\".",
8551+
"maxLength": 1000,
85518552
"type": "string"
85528553
},
85538554
"objectTransformation": {

agents-manage-ui/src/components/triggers/trigger-form.tsx

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use client';
22

33
import { zodResolver } from '@hookform/resolvers/zod';
4+
import {
5+
validateJMESPath as coreValidateJMESPath,
6+
validateRegex as coreValidateRegex,
7+
} from '@inkeep/agents-core/utils/signature-validation';
48
import { ArrowDown, ArrowUp, Check, ChevronDown, KeyRound, Plus, Trash2 } from 'lucide-react';
59
import { useRouter } from 'next/navigation';
610
import { useEffect, useMemo, useState } from 'react';
@@ -33,6 +37,19 @@ import { fetchCredentialsAction } from '@/lib/actions/credentials';
3337
import { createTriggerAction, updateTriggerAction } from '@/lib/actions/triggers';
3438
import type { Trigger } from '@/lib/api/triggers';
3539

40+
// Adapter functions that convert ValidationResult to string | undefined for form validation
41+
const validateJMESPath = (value: string): string | undefined => {
42+
if (!value.trim()) return undefined;
43+
const result = coreValidateJMESPath(value);
44+
return result.valid ? undefined : result.error;
45+
};
46+
47+
const validateRegex = (value: string): string | undefined => {
48+
if (!value.trim()) return undefined;
49+
const result = coreValidateRegex(value);
50+
return result.valid ? undefined : result.error;
51+
};
52+
3653
// Transform type options
3754
const transformTypeOptions: SelectOption[] = [
3855
{ value: 'none', label: 'None' },
@@ -408,33 +425,6 @@ export function TriggerForm({ tenantId, projectId, agentId, trigger, mode }: Tri
408425
const transformType = form.watch('transformType');
409426
const signatureSource = form.watch('signatureSource');
410427

411-
// Validation functions for JMESPath and regex
412-
const validateJMESPath = (value: string): string | undefined => {
413-
if (!value.trim()) return undefined;
414-
415-
try {
416-
// Simple JMESPath validation - check for basic syntax issues
417-
// Full validation happens on the backend
418-
if (value.includes('..') || value.includes('[[')) {
419-
return 'Invalid JMESPath syntax';
420-
}
421-
return undefined;
422-
} catch {
423-
return 'Invalid JMESPath expression';
424-
}
425-
};
426-
427-
const validateRegex = (value: string): string | undefined => {
428-
if (!value.trim()) return undefined;
429-
430-
try {
431-
new RegExp(value);
432-
return undefined;
433-
} catch {
434-
return 'Invalid regular expression';
435-
}
436-
};
437-
438428
// Apply provider preset to form fields
439429
const applyPreset = (presetKey: string) => {
440430
const preset = providerPresets[presetKey];

packages/agents-core/src/context/TemplateEngine.ts

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import jmespath from 'jmespath';
1+
import { normalizeJMESPath, searchJMESPath } from '../utils/jmespath-utils';
22
import { getLogger } from '../utils/logger';
33

44
const logger = getLogger('template-engine');
@@ -52,35 +52,6 @@ export class TemplateEngine {
5252
}
5353
}
5454

55-
/**
56-
* Normalize JMES path by wrapping property names with dashes in quotes
57-
* Example: headers.x-tenant-id -> headers."x-tenant-id"
58-
* Example: api-responses[0].response-code -> "api-responses"[0]."response-code"
59-
*/
60-
private static normalizeJMESPath(path: string): string {
61-
const segments = path.split('.');
62-
return segments
63-
.map((segment) => {
64-
if (!segment.includes('-')) {
65-
return segment;
66-
}
67-
68-
if (segment.startsWith('"') && segment.includes('"')) {
69-
return segment;
70-
}
71-
72-
const bracketIndex = segment.indexOf('[');
73-
if (bracketIndex !== -1) {
74-
const propertyName = segment.substring(0, bracketIndex);
75-
const arrayAccess = segment.substring(bracketIndex);
76-
return `"${propertyName}"${arrayAccess}`;
77-
}
78-
79-
return `"${segment}"`;
80-
})
81-
.join('.');
82-
}
83-
8455
/**
8556
* Process variable substitutions {{variable.path}} using JMESPath
8657
*/
@@ -99,10 +70,10 @@ export class TemplateEngine {
9970
}
10071

10172
// Normalize path to handle dashes in property names
102-
const normalizedPath = TemplateEngine.normalizeJMESPath(trimmedPath);
73+
const normalizedPath = normalizeJMESPath(trimmedPath);
10374

10475
// Use JMESPath to extract value from context
105-
const result = jmespath.search(context, normalizedPath);
76+
const result = searchJMESPath(context, normalizedPath);
10677

10778
if (result === undefined || result === null) {
10879
if (options.strict) {
@@ -147,7 +118,7 @@ export class TemplateEngine {
147118
return String(result);
148119
} catch (error) {
149120
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
150-
const normalizedPath = TemplateEngine.normalizeJMESPath(trimmedPath);
121+
const normalizedPath = normalizeJMESPath(trimmedPath);
151122

152123
if (options.strict) {
153124
throw new Error(

packages/agents-core/src/utils/JsonTransformer.ts

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import * as jmespath from 'jmespath';
2-
3-
// TypeScript workaround for missing compile method in type definitions
4-
interface JMESPathExtended {
5-
search: typeof jmespath.search;
6-
compile: (expression: string) => any;
7-
}
8-
9-
const jmespathExt = jmespath as unknown as JMESPathExtended;
10-
2+
import {
3+
compileJMESPath,
4+
DANGEROUS_PATTERNS,
5+
MAX_EXPRESSION_LENGTH,
6+
validateJMESPathSecure,
7+
} from './jmespath-utils';
118
import { getLogger } from './logger';
129

1310
const logger = getLogger('JsonTransformer');
@@ -20,45 +17,35 @@ interface TransformOptions {
2017

2118
export class JsonTransformer {
2219
private static readonly DEFAULT_TIMEOUT = 5000; // 5 seconds
23-
private static readonly MAX_EXPRESSION_LENGTH = 1000;
24-
private static readonly DANGEROUS_PATTERNS = [
25-
/\$\{.*\}/, // Template injection
26-
/eval\s*\(/, // Eval calls
27-
/function\s*\(/, // Function definitions
28-
/constructor/, // Constructor access
29-
/prototype/, // Prototype manipulation
30-
/__proto__/, // Proto access
31-
];
3220

3321
/**
3422
* Validate JMESPath expression for security and correctness
3523
*/
36-
private static validateJMESPath(expression: string, _allowedFunctions?: string[]): void {
24+
private static validateExpression(expression: string, _allowedFunctions?: string[]): void {
3725
if (!expression || typeof expression !== 'string') {
3826
throw new Error('JMESPath expression must be a non-empty string');
3927
}
4028

41-
if (expression.length > JsonTransformer.MAX_EXPRESSION_LENGTH) {
42-
throw new Error(
43-
`JMESPath expression too long (max ${JsonTransformer.MAX_EXPRESSION_LENGTH} characters)`
44-
);
29+
// Check length first
30+
if (expression.length > MAX_EXPRESSION_LENGTH) {
31+
throw new Error(`JMESPath expression too long (max ${MAX_EXPRESSION_LENGTH} characters)`);
4532
}
4633

47-
// Check for dangerous patterns
48-
for (const pattern of JsonTransformer.DANGEROUS_PATTERNS) {
34+
// Check dangerous patterns
35+
for (const pattern of DANGEROUS_PATTERNS) {
4936
if (pattern.test(expression)) {
5037
throw new Error(`JMESPath expression contains dangerous pattern: ${pattern.source}`);
5138
}
5239
}
5340

54-
// Basic syntax validation - try to compile the expression
55-
try {
56-
// Use compile to validate syntax without requiring specific data
57-
jmespathExt.compile(expression);
58-
} catch (error) {
59-
throw new Error(
60-
`Invalid JMESPath syntax: ${error instanceof Error ? error.message : String(error)}`
61-
);
41+
// Use validateJMESPathSecure for syntax validation (patterns already checked above)
42+
const result = validateJMESPathSecure(expression, {
43+
maxLength: MAX_EXPRESSION_LENGTH + 1, // Skip length check (already done)
44+
dangerousPatterns: [], // Skip pattern check (already done)
45+
});
46+
47+
if (!result.valid) {
48+
throw new Error(`Invalid JMESPath syntax: ${result.error}`);
6249
}
6350

6451
logger.debug('JMESPath expression validated', `${expression.substring(0, 100)}...`);
@@ -95,7 +82,7 @@ export class JsonTransformer {
9582
const { timeout = JsonTransformer.DEFAULT_TIMEOUT, allowedFunctions } = options;
9683

9784
// Validate expression before execution
98-
JsonTransformer.validateJMESPath(jmesPathExpression, allowedFunctions);
85+
JsonTransformer.validateExpression(jmesPathExpression, allowedFunctions);
9986

10087
try {
10188
logger.debug(
@@ -137,8 +124,7 @@ export class JsonTransformer {
137124
}
138125
// Validate each path is a valid JMESPath expression
139126
try {
140-
// Use compile to validate syntax without requiring specific data
141-
jmespathExt.compile(path);
127+
compileJMESPath(path);
142128
} catch (error) {
143129
throw new Error(
144130
`Invalid JMESPath in object transformation value "${path}": ${error instanceof Error ? error.message : String(error)}`

0 commit comments

Comments
 (0)