@artemiskit/sdk 0.3.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.
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Action Validator
3
+ *
4
+ * Validates tool/function calls before execution.
5
+ * Ensures agents only perform allowed actions with valid parameters.
6
+ */
7
+
8
+ import { nanoid } from 'nanoid';
9
+ import type {
10
+ ActionDefinition,
11
+ ActionParameter,
12
+ GuardrailResult,
13
+ InterceptedToolCall,
14
+ ParameterValidation,
15
+ Violation,
16
+ ViolationSeverity,
17
+ } from './types';
18
+
19
+ /**
20
+ * Action validation result
21
+ */
22
+ export interface ActionValidationResult {
23
+ valid: boolean;
24
+ violations: Violation[];
25
+ sanitizedArguments?: Record<string, unknown>;
26
+ requiresApproval?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Action validator configuration
31
+ */
32
+ export interface ActionValidatorConfig {
33
+ /** Allowed actions with their definitions */
34
+ allowedActions?: ActionDefinition[];
35
+ /** Default behavior for undefined actions */
36
+ defaultAllow?: boolean;
37
+ /** Default risk level for undefined actions */
38
+ defaultRiskLevel?: ViolationSeverity;
39
+ /** Block high-risk actions automatically */
40
+ blockHighRisk?: boolean;
41
+ /** Custom validation function */
42
+ customValidator?: (toolCall: InterceptedToolCall) => Promise<ActionValidationResult>;
43
+ }
44
+
45
+ /**
46
+ * Action Validator
47
+ *
48
+ * Validates tool/function calls against defined policies.
49
+ */
50
+ export class ActionValidator {
51
+ private config: ActionValidatorConfig;
52
+ private actionMap: Map<string, ActionDefinition>;
53
+ private callHistory: InterceptedToolCall[];
54
+ private callCounts: Map<string, { count: number; windowStart: number }>;
55
+
56
+ constructor(config: ActionValidatorConfig = {}) {
57
+ this.config = {
58
+ defaultAllow: false,
59
+ defaultRiskLevel: 'medium',
60
+ blockHighRisk: true,
61
+ ...config,
62
+ };
63
+ this.actionMap = new Map();
64
+ this.callHistory = [];
65
+ this.callCounts = new Map();
66
+
67
+ // Index allowed actions
68
+ if (config.allowedActions) {
69
+ for (const action of config.allowedActions) {
70
+ this.actionMap.set(action.name, action);
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Validate a tool/function call
77
+ */
78
+ async validate(toolCall: InterceptedToolCall): Promise<ActionValidationResult> {
79
+ const violations: Violation[] = [];
80
+ let sanitizedArguments = { ...toolCall.arguments };
81
+ let requiresApproval = false;
82
+
83
+ // Record the call
84
+ this.callHistory.push(toolCall);
85
+
86
+ // Get action definition
87
+ const actionDef = this.actionMap.get(toolCall.toolName);
88
+
89
+ // Check if action is defined
90
+ if (!actionDef) {
91
+ if (!this.config.defaultAllow) {
92
+ violations.push({
93
+ id: nanoid(),
94
+ type: 'action_validation',
95
+ severity: this.config.defaultRiskLevel ?? 'medium',
96
+ message: `Unknown action: ${toolCall.toolName}`,
97
+ details: { toolName: toolCall.toolName },
98
+ timestamp: new Date(),
99
+ action: 'block',
100
+ blocked: true,
101
+ });
102
+ }
103
+ } else {
104
+ // Check if action is allowed
105
+ if (actionDef.allowed === false) {
106
+ violations.push({
107
+ id: nanoid(),
108
+ type: 'action_validation',
109
+ severity: actionDef.riskLevel ?? 'high',
110
+ message: `Action not allowed: ${toolCall.toolName}`,
111
+ details: { toolName: toolCall.toolName, reason: 'explicitly_disabled' },
112
+ timestamp: new Date(),
113
+ action: 'block',
114
+ blocked: true,
115
+ });
116
+ }
117
+
118
+ // Check rate limits
119
+ const rateLimitViolation = this.checkRateLimit(toolCall.toolName, actionDef);
120
+ if (rateLimitViolation) {
121
+ violations.push(rateLimitViolation);
122
+ }
123
+
124
+ // Check if requires approval
125
+ if (actionDef.requiresApproval) {
126
+ requiresApproval = true;
127
+ }
128
+
129
+ // Block high-risk actions if configured
130
+ if (
131
+ this.config.blockHighRisk &&
132
+ (actionDef.riskLevel === 'high' || actionDef.riskLevel === 'critical')
133
+ ) {
134
+ if (!requiresApproval) {
135
+ // Auto-block unless approval is required
136
+ violations.push({
137
+ id: nanoid(),
138
+ type: 'action_validation',
139
+ severity: actionDef.riskLevel,
140
+ message: `High-risk action blocked: ${toolCall.toolName}`,
141
+ details: { toolName: toolCall.toolName, riskLevel: actionDef.riskLevel },
142
+ timestamp: new Date(),
143
+ action: 'block',
144
+ blocked: true,
145
+ });
146
+ }
147
+ }
148
+
149
+ // Validate parameters
150
+ if (actionDef.parameters) {
151
+ const paramResult = this.validateParameters(toolCall.arguments, actionDef.parameters);
152
+ violations.push(...paramResult.violations);
153
+ if (paramResult.sanitized) {
154
+ sanitizedArguments = paramResult.sanitized;
155
+ }
156
+ }
157
+ }
158
+
159
+ // Run custom validator if provided
160
+ if (this.config.customValidator) {
161
+ const customResult = await this.config.customValidator(toolCall);
162
+ violations.push(...customResult.violations);
163
+ if (customResult.sanitizedArguments) {
164
+ sanitizedArguments = customResult.sanitizedArguments;
165
+ }
166
+ if (customResult.requiresApproval) {
167
+ requiresApproval = true;
168
+ }
169
+ }
170
+
171
+ return {
172
+ valid: violations.length === 0,
173
+ violations,
174
+ sanitizedArguments,
175
+ requiresApproval,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Create a guardrail function from this validator
181
+ */
182
+ asGuardrail(): (content: string, context?: Record<string, unknown>) => Promise<GuardrailResult> {
183
+ return async (_content: string, context?: Record<string, unknown>) => {
184
+ // Extract tool call from context if available
185
+ const toolCall = context?.toolCall as InterceptedToolCall | undefined;
186
+
187
+ if (!toolCall) {
188
+ return { passed: true, violations: [] };
189
+ }
190
+
191
+ const result = await this.validate(toolCall);
192
+ return {
193
+ passed: result.valid,
194
+ violations: result.violations,
195
+ };
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Register an allowed action
201
+ */
202
+ registerAction(action: ActionDefinition): void {
203
+ this.actionMap.set(action.name, action);
204
+ }
205
+
206
+ /**
207
+ * Remove an action from allowed list
208
+ */
209
+ unregisterAction(name: string): void {
210
+ this.actionMap.delete(name);
211
+ }
212
+
213
+ /**
214
+ * Get all registered actions
215
+ */
216
+ getRegisteredActions(): ActionDefinition[] {
217
+ return Array.from(this.actionMap.values());
218
+ }
219
+
220
+ /**
221
+ * Get call history
222
+ */
223
+ getCallHistory(): InterceptedToolCall[] {
224
+ return [...this.callHistory];
225
+ }
226
+
227
+ /**
228
+ * Clear call history
229
+ */
230
+ clearHistory(): void {
231
+ this.callHistory = [];
232
+ this.callCounts.clear();
233
+ }
234
+
235
+ /**
236
+ * Check rate limit for an action
237
+ */
238
+ private checkRateLimit(toolName: string, actionDef: ActionDefinition): Violation | null {
239
+ if (!actionDef.maxCallsPerMinute) {
240
+ return null;
241
+ }
242
+
243
+ const now = Date.now();
244
+ const windowMs = 60000; // 1 minute
245
+ let entry = this.callCounts.get(toolName);
246
+
247
+ if (!entry || now - entry.windowStart >= windowMs) {
248
+ // Start new window
249
+ entry = { count: 1, windowStart: now };
250
+ this.callCounts.set(toolName, entry);
251
+ return null;
252
+ }
253
+
254
+ entry.count++;
255
+
256
+ if (entry.count > actionDef.maxCallsPerMinute) {
257
+ return {
258
+ id: nanoid(),
259
+ type: 'rate_limit',
260
+ severity: 'medium',
261
+ message: `Rate limit exceeded for action: ${toolName}`,
262
+ details: {
263
+ toolName,
264
+ limit: actionDef.maxCallsPerMinute,
265
+ current: entry.count,
266
+ },
267
+ timestamp: new Date(),
268
+ action: 'block',
269
+ blocked: true,
270
+ };
271
+ }
272
+
273
+ return null;
274
+ }
275
+
276
+ /**
277
+ * Validate parameters against definitions
278
+ */
279
+ private validateParameters(
280
+ args: Record<string, unknown>,
281
+ params: ActionParameter[]
282
+ ): { violations: Violation[]; sanitized: Record<string, unknown> | null } {
283
+ const violations: Violation[] = [];
284
+ const sanitized = { ...args };
285
+
286
+ for (const param of params) {
287
+ const value = args[param.name];
288
+
289
+ // Check required
290
+ if (param.required && (value === undefined || value === null)) {
291
+ violations.push({
292
+ id: nanoid(),
293
+ type: 'action_validation',
294
+ severity: 'medium',
295
+ message: `Missing required parameter: ${param.name}`,
296
+ details: { parameter: param.name },
297
+ timestamp: new Date(),
298
+ action: 'block',
299
+ blocked: true,
300
+ });
301
+ continue;
302
+ }
303
+
304
+ if (value === undefined || value === null) {
305
+ continue;
306
+ }
307
+
308
+ // Type checking
309
+ if (!this.checkType(value, param.type)) {
310
+ violations.push({
311
+ id: nanoid(),
312
+ type: 'action_validation',
313
+ severity: 'medium',
314
+ message: `Invalid type for parameter: ${param.name} (expected ${param.type})`,
315
+ details: { parameter: param.name, expected: param.type, actual: typeof value },
316
+ timestamp: new Date(),
317
+ action: 'block',
318
+ blocked: true,
319
+ });
320
+ }
321
+
322
+ // Run validation rules
323
+ if (param.validation) {
324
+ const validationViolations = this.runValidation(param.name, value, param.validation);
325
+ violations.push(...validationViolations);
326
+ }
327
+ }
328
+
329
+ return {
330
+ violations,
331
+ sanitized: violations.length === 0 ? sanitized : null,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Check if value matches expected type
337
+ */
338
+ private checkType(value: unknown, expectedType: string): boolean {
339
+ switch (expectedType) {
340
+ case 'string':
341
+ return typeof value === 'string';
342
+ case 'number':
343
+ return typeof value === 'number';
344
+ case 'boolean':
345
+ return typeof value === 'boolean';
346
+ case 'array':
347
+ return Array.isArray(value);
348
+ case 'object':
349
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
350
+ default:
351
+ return true;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Run validation rules on a parameter value
357
+ */
358
+ private runValidation(
359
+ paramName: string,
360
+ value: unknown,
361
+ validation: ParameterValidation
362
+ ): Violation[] {
363
+ const violations: Violation[] = [];
364
+ const strValue = String(value);
365
+
366
+ // Pattern matching
367
+ if (validation.pattern) {
368
+ const regex = new RegExp(validation.pattern);
369
+ if (!regex.test(strValue)) {
370
+ violations.push({
371
+ id: nanoid(),
372
+ type: 'action_validation',
373
+ severity: 'medium',
374
+ message: `Parameter ${paramName} does not match required pattern`,
375
+ details: { parameter: paramName, pattern: validation.pattern },
376
+ timestamp: new Date(),
377
+ action: 'block',
378
+ blocked: true,
379
+ });
380
+ }
381
+ }
382
+
383
+ // Length checks for strings
384
+ if (typeof value === 'string') {
385
+ if (validation.minLength && value.length < validation.minLength) {
386
+ violations.push({
387
+ id: nanoid(),
388
+ type: 'action_validation',
389
+ severity: 'low',
390
+ message: `Parameter ${paramName} is too short (min: ${validation.minLength})`,
391
+ details: { parameter: paramName, minLength: validation.minLength },
392
+ timestamp: new Date(),
393
+ action: 'warn',
394
+ blocked: false,
395
+ });
396
+ }
397
+ if (validation.maxLength && value.length > validation.maxLength) {
398
+ violations.push({
399
+ id: nanoid(),
400
+ type: 'action_validation',
401
+ severity: 'medium',
402
+ message: `Parameter ${paramName} is too long (max: ${validation.maxLength})`,
403
+ details: { parameter: paramName, maxLength: validation.maxLength },
404
+ timestamp: new Date(),
405
+ action: 'block',
406
+ blocked: true,
407
+ });
408
+ }
409
+ }
410
+
411
+ // Numeric range checks
412
+ if (typeof value === 'number') {
413
+ if (validation.minValue !== undefined && value < validation.minValue) {
414
+ violations.push({
415
+ id: nanoid(),
416
+ type: 'action_validation',
417
+ severity: 'medium',
418
+ message: `Parameter ${paramName} is below minimum (min: ${validation.minValue})`,
419
+ details: { parameter: paramName, minValue: validation.minValue },
420
+ timestamp: new Date(),
421
+ action: 'block',
422
+ blocked: true,
423
+ });
424
+ }
425
+ if (validation.maxValue !== undefined && value > validation.maxValue) {
426
+ violations.push({
427
+ id: nanoid(),
428
+ type: 'action_validation',
429
+ severity: 'medium',
430
+ message: `Parameter ${paramName} exceeds maximum (max: ${validation.maxValue})`,
431
+ details: { parameter: paramName, maxValue: validation.maxValue },
432
+ timestamp: new Date(),
433
+ action: 'block',
434
+ blocked: true,
435
+ });
436
+ }
437
+ }
438
+
439
+ // Allowed values check
440
+ if (validation.allowedValues && !validation.allowedValues.includes(value as string | number)) {
441
+ violations.push({
442
+ id: nanoid(),
443
+ type: 'action_validation',
444
+ severity: 'medium',
445
+ message: `Parameter ${paramName} has invalid value`,
446
+ details: {
447
+ parameter: paramName,
448
+ value,
449
+ allowedValues: validation.allowedValues,
450
+ },
451
+ timestamp: new Date(),
452
+ action: 'block',
453
+ blocked: true,
454
+ });
455
+ }
456
+
457
+ // Blocked values check
458
+ if (validation.blockedValues?.includes(value as string | number)) {
459
+ violations.push({
460
+ id: nanoid(),
461
+ type: 'action_validation',
462
+ severity: 'high',
463
+ message: `Parameter ${paramName} contains blocked value`,
464
+ details: { parameter: paramName, value },
465
+ timestamp: new Date(),
466
+ action: 'block',
467
+ blocked: true,
468
+ });
469
+ }
470
+
471
+ // Blocked patterns check
472
+ if (validation.blockedPatterns) {
473
+ for (const pattern of validation.blockedPatterns) {
474
+ const regex = new RegExp(pattern, 'i');
475
+ if (regex.test(strValue)) {
476
+ violations.push({
477
+ id: nanoid(),
478
+ type: 'action_validation',
479
+ severity: 'high',
480
+ message: `Parameter ${paramName} matches blocked pattern`,
481
+ details: { parameter: paramName, pattern },
482
+ timestamp: new Date(),
483
+ action: 'block',
484
+ blocked: true,
485
+ });
486
+ }
487
+ }
488
+ }
489
+
490
+ return violations;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Create a pre-configured action validator with common dangerous actions blocked
496
+ */
497
+ export function createDefaultActionValidator(): ActionValidator {
498
+ return new ActionValidator({
499
+ defaultAllow: true,
500
+ blockHighRisk: true,
501
+ allowedActions: [
502
+ // File operations - high risk
503
+ {
504
+ name: 'delete_file',
505
+ description: 'Delete a file from the filesystem',
506
+ category: 'filesystem',
507
+ riskLevel: 'critical',
508
+ allowed: false,
509
+ },
510
+ {
511
+ name: 'write_file',
512
+ description: 'Write content to a file',
513
+ category: 'filesystem',
514
+ riskLevel: 'high',
515
+ requiresApproval: true,
516
+ parameters: [
517
+ {
518
+ name: 'path',
519
+ type: 'string',
520
+ required: true,
521
+ validation: {
522
+ blockedPatterns: ['\\.env', 'credentials', 'password', 'secret', '/etc/', '/root/'],
523
+ },
524
+ },
525
+ ],
526
+ },
527
+ // Network operations
528
+ {
529
+ name: 'http_request',
530
+ description: 'Make an HTTP request',
531
+ category: 'network',
532
+ riskLevel: 'medium',
533
+ maxCallsPerMinute: 60,
534
+ parameters: [
535
+ {
536
+ name: 'url',
537
+ type: 'string',
538
+ required: true,
539
+ validation: {
540
+ blockedPatterns: [
541
+ 'localhost',
542
+ '127\\.0\\.0\\.1',
543
+ '0\\.0\\.0\\.0',
544
+ 'internal',
545
+ '\\.local',
546
+ ],
547
+ },
548
+ },
549
+ ],
550
+ },
551
+ // Database operations
552
+ {
553
+ name: 'execute_sql',
554
+ description: 'Execute SQL query',
555
+ category: 'database',
556
+ riskLevel: 'critical',
557
+ allowed: false,
558
+ },
559
+ {
560
+ name: 'query_database',
561
+ description: 'Read-only database query',
562
+ category: 'database',
563
+ riskLevel: 'medium',
564
+ maxCallsPerMinute: 100,
565
+ },
566
+ // System operations
567
+ {
568
+ name: 'execute_command',
569
+ description: 'Execute a shell command',
570
+ category: 'system',
571
+ riskLevel: 'critical',
572
+ allowed: false,
573
+ },
574
+ // Email/messaging
575
+ {
576
+ name: 'send_email',
577
+ description: 'Send an email',
578
+ category: 'communication',
579
+ riskLevel: 'high',
580
+ requiresApproval: true,
581
+ maxCallsPerMinute: 10,
582
+ },
583
+ ],
584
+ });
585
+ }