family-ai-agent 1.0.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 (132) hide show
  1. package/.env.example +49 -0
  2. package/README.md +161 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +336 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/config/index.d.ts +37 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/config/index.js +68 -0
  10. package/dist/config/index.js.map +1 -0
  11. package/dist/config/models.d.ts +17 -0
  12. package/dist/config/models.d.ts.map +1 -0
  13. package/dist/config/models.js +128 -0
  14. package/dist/config/models.js.map +1 -0
  15. package/dist/core/agents/agent-factory.d.ts +31 -0
  16. package/dist/core/agents/agent-factory.d.ts.map +1 -0
  17. package/dist/core/agents/agent-factory.js +151 -0
  18. package/dist/core/agents/agent-factory.js.map +1 -0
  19. package/dist/core/agents/base-agent.d.ts +51 -0
  20. package/dist/core/agents/base-agent.d.ts.map +1 -0
  21. package/dist/core/agents/base-agent.js +245 -0
  22. package/dist/core/agents/base-agent.js.map +1 -0
  23. package/dist/core/agents/index.d.ts +8 -0
  24. package/dist/core/agents/index.d.ts.map +1 -0
  25. package/dist/core/agents/index.js +9 -0
  26. package/dist/core/agents/index.js.map +1 -0
  27. package/dist/core/agents/personalities/automation.d.ts +14 -0
  28. package/dist/core/agents/personalities/automation.d.ts.map +1 -0
  29. package/dist/core/agents/personalities/automation.js +146 -0
  30. package/dist/core/agents/personalities/automation.js.map +1 -0
  31. package/dist/core/agents/personalities/chat.d.ts +10 -0
  32. package/dist/core/agents/personalities/chat.d.ts.map +1 -0
  33. package/dist/core/agents/personalities/chat.js +132 -0
  34. package/dist/core/agents/personalities/chat.js.map +1 -0
  35. package/dist/core/agents/personalities/coding.d.ts +16 -0
  36. package/dist/core/agents/personalities/coding.d.ts.map +1 -0
  37. package/dist/core/agents/personalities/coding.js +166 -0
  38. package/dist/core/agents/personalities/coding.js.map +1 -0
  39. package/dist/core/agents/personalities/research.d.ts +13 -0
  40. package/dist/core/agents/personalities/research.d.ts.map +1 -0
  41. package/dist/core/agents/personalities/research.js +133 -0
  42. package/dist/core/agents/personalities/research.js.map +1 -0
  43. package/dist/core/agents/types.d.ts +102 -0
  44. package/dist/core/agents/types.d.ts.map +1 -0
  45. package/dist/core/agents/types.js +2 -0
  46. package/dist/core/agents/types.js.map +1 -0
  47. package/dist/core/orchestrator/graph.d.ts +118 -0
  48. package/dist/core/orchestrator/graph.d.ts.map +1 -0
  49. package/dist/core/orchestrator/graph.js +233 -0
  50. package/dist/core/orchestrator/graph.js.map +1 -0
  51. package/dist/database/client.d.ts +19 -0
  52. package/dist/database/client.d.ts.map +1 -0
  53. package/dist/database/client.js +95 -0
  54. package/dist/database/client.js.map +1 -0
  55. package/dist/index.d.ts +41 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +67 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/llm/openrouter-client.d.ts +45 -0
  60. package/dist/llm/openrouter-client.d.ts.map +1 -0
  61. package/dist/llm/openrouter-client.js +155 -0
  62. package/dist/llm/openrouter-client.js.map +1 -0
  63. package/dist/memory/conversation/index.d.ts +37 -0
  64. package/dist/memory/conversation/index.d.ts.map +1 -0
  65. package/dist/memory/conversation/index.js +196 -0
  66. package/dist/memory/conversation/index.js.map +1 -0
  67. package/dist/memory/index.d.ts +4 -0
  68. package/dist/memory/index.d.ts.map +1 -0
  69. package/dist/memory/index.js +5 -0
  70. package/dist/memory/index.js.map +1 -0
  71. package/dist/memory/knowledge-base/index.d.ts +51 -0
  72. package/dist/memory/knowledge-base/index.d.ts.map +1 -0
  73. package/dist/memory/knowledge-base/index.js +222 -0
  74. package/dist/memory/knowledge-base/index.js.map +1 -0
  75. package/dist/memory/longterm/vector-store.d.ts +44 -0
  76. package/dist/memory/longterm/vector-store.d.ts.map +1 -0
  77. package/dist/memory/longterm/vector-store.js +229 -0
  78. package/dist/memory/longterm/vector-store.js.map +1 -0
  79. package/dist/safety/audit-logger.d.ts +68 -0
  80. package/dist/safety/audit-logger.d.ts.map +1 -0
  81. package/dist/safety/audit-logger.js +215 -0
  82. package/dist/safety/audit-logger.js.map +1 -0
  83. package/dist/safety/guardrails/input-guardrail.d.ts +21 -0
  84. package/dist/safety/guardrails/input-guardrail.d.ts.map +1 -0
  85. package/dist/safety/guardrails/input-guardrail.js +145 -0
  86. package/dist/safety/guardrails/input-guardrail.js.map +1 -0
  87. package/dist/safety/guardrails/output-guardrail.d.ts +18 -0
  88. package/dist/safety/guardrails/output-guardrail.d.ts.map +1 -0
  89. package/dist/safety/guardrails/output-guardrail.js +125 -0
  90. package/dist/safety/guardrails/output-guardrail.js.map +1 -0
  91. package/dist/safety/index.d.ts +4 -0
  92. package/dist/safety/index.d.ts.map +1 -0
  93. package/dist/safety/index.js +5 -0
  94. package/dist/safety/index.js.map +1 -0
  95. package/dist/utils/errors.d.ts +36 -0
  96. package/dist/utils/errors.d.ts.map +1 -0
  97. package/dist/utils/errors.js +94 -0
  98. package/dist/utils/errors.js.map +1 -0
  99. package/dist/utils/logger.d.ts +8 -0
  100. package/dist/utils/logger.d.ts.map +1 -0
  101. package/dist/utils/logger.js +47 -0
  102. package/dist/utils/logger.js.map +1 -0
  103. package/docker/init-db.sql +149 -0
  104. package/docker/sandbox/Dockerfile.sandbox +29 -0
  105. package/docker-compose.yml +61 -0
  106. package/package.json +80 -0
  107. package/src/cli/index.ts +392 -0
  108. package/src/config/index.ts +85 -0
  109. package/src/config/models.ts +156 -0
  110. package/src/core/agents/agent-factory.ts +192 -0
  111. package/src/core/agents/base-agent.ts +333 -0
  112. package/src/core/agents/index.ts +27 -0
  113. package/src/core/agents/personalities/automation.ts +202 -0
  114. package/src/core/agents/personalities/chat.ts +159 -0
  115. package/src/core/agents/personalities/coding.ts +227 -0
  116. package/src/core/agents/personalities/research.ts +177 -0
  117. package/src/core/agents/types.ts +124 -0
  118. package/src/core/orchestrator/graph.ts +305 -0
  119. package/src/database/client.ts +109 -0
  120. package/src/index.ts +104 -0
  121. package/src/llm/openrouter-client.ts +218 -0
  122. package/src/memory/conversation/index.ts +313 -0
  123. package/src/memory/index.ts +23 -0
  124. package/src/memory/knowledge-base/index.ts +357 -0
  125. package/src/memory/longterm/vector-store.ts +364 -0
  126. package/src/safety/audit-logger.ts +357 -0
  127. package/src/safety/guardrails/input-guardrail.ts +191 -0
  128. package/src/safety/guardrails/output-guardrail.ts +160 -0
  129. package/src/safety/index.ts +21 -0
  130. package/src/utils/errors.ts +120 -0
  131. package/src/utils/logger.ts +74 -0
  132. package/tsconfig.json +37 -0
@@ -0,0 +1,357 @@
1
+ import { createHash } from 'crypto';
2
+ import { query } from '../database/client.js';
3
+ import { config } from '../config/index.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const logger = createLogger('AuditLogger');
7
+
8
+ export interface AuditLogEntry {
9
+ id: string;
10
+ userId?: string;
11
+ agentId?: string;
12
+ actionType: string;
13
+ actionDetails: Record<string, unknown>;
14
+ inputHash?: string;
15
+ outputHash?: string;
16
+ status: 'success' | 'failure' | 'blocked';
17
+ errorMessage?: string;
18
+ executionTimeMs?: number;
19
+ createdAt: Date;
20
+ }
21
+
22
+ export type AuditActionType =
23
+ | 'user_input'
24
+ | 'agent_execution'
25
+ | 'tool_call'
26
+ | 'memory_access'
27
+ | 'document_upload'
28
+ | 'safety_block'
29
+ | 'error'
30
+ | 'handoff'
31
+ | 'api_request';
32
+
33
+ export class AuditLogger {
34
+ private enabled: boolean;
35
+
36
+ constructor() {
37
+ this.enabled = config.ENABLE_AUDIT_LOGGING;
38
+ }
39
+
40
+ // Hash sensitive content for logging
41
+ private hashContent(content: string): string {
42
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
43
+ }
44
+
45
+ // Log an action
46
+ async log(
47
+ actionType: AuditActionType,
48
+ details: Record<string, unknown>,
49
+ options: {
50
+ userId?: string;
51
+ agentId?: string;
52
+ input?: string;
53
+ output?: string;
54
+ status?: 'success' | 'failure' | 'blocked';
55
+ error?: string;
56
+ executionTimeMs?: number;
57
+ } = {}
58
+ ): Promise<string | null> {
59
+ if (!this.enabled) {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ const result = await query<{ id: string }>(
65
+ `INSERT INTO audit_logs
66
+ (user_id, agent_id, action_type, action_details, input_hash, output_hash, status, error_message, execution_time_ms)
67
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
68
+ RETURNING id`,
69
+ [
70
+ options.userId ?? null,
71
+ options.agentId ?? null,
72
+ actionType,
73
+ JSON.stringify(details),
74
+ options.input ? this.hashContent(options.input) : null,
75
+ options.output ? this.hashContent(options.output) : null,
76
+ options.status ?? 'success',
77
+ options.error ?? null,
78
+ options.executionTimeMs ?? null,
79
+ ]
80
+ );
81
+
82
+ const logId = result.rows[0]?.id;
83
+ logger.debug('Audit log created', { logId, actionType });
84
+ return logId ?? null;
85
+ } catch (error) {
86
+ logger.error('Failed to create audit log', { error, actionType });
87
+ // Don't throw - audit logging should not break the main flow
88
+ return null;
89
+ }
90
+ }
91
+
92
+ // Log user input
93
+ async logUserInput(
94
+ input: string,
95
+ options: { userId?: string; metadata?: Record<string, unknown> } = {}
96
+ ): Promise<string | null> {
97
+ return this.log(
98
+ 'user_input',
99
+ {
100
+ inputLength: input.length,
101
+ ...options.metadata,
102
+ },
103
+ {
104
+ userId: options.userId,
105
+ input,
106
+ status: 'success',
107
+ }
108
+ );
109
+ }
110
+
111
+ // Log agent execution
112
+ async logAgentExecution(
113
+ agentId: string,
114
+ input: string,
115
+ output: string,
116
+ executionTimeMs: number,
117
+ options: { userId?: string; success?: boolean; error?: string } = {}
118
+ ): Promise<string | null> {
119
+ return this.log(
120
+ 'agent_execution',
121
+ {
122
+ inputLength: input.length,
123
+ outputLength: output.length,
124
+ executionTimeMs,
125
+ },
126
+ {
127
+ userId: options.userId,
128
+ agentId,
129
+ input,
130
+ output,
131
+ status: options.success !== false ? 'success' : 'failure',
132
+ error: options.error,
133
+ executionTimeMs,
134
+ }
135
+ );
136
+ }
137
+
138
+ // Log tool call
139
+ async logToolCall(
140
+ toolName: string,
141
+ input: unknown,
142
+ output: unknown,
143
+ options: {
144
+ agentId?: string;
145
+ userId?: string;
146
+ success?: boolean;
147
+ error?: string;
148
+ executionTimeMs?: number;
149
+ } = {}
150
+ ): Promise<string | null> {
151
+ return this.log(
152
+ 'tool_call',
153
+ {
154
+ toolName,
155
+ inputType: typeof input,
156
+ outputType: typeof output,
157
+ },
158
+ {
159
+ agentId: options.agentId,
160
+ userId: options.userId,
161
+ input: JSON.stringify(input),
162
+ output: JSON.stringify(output),
163
+ status: options.success !== false ? 'success' : 'failure',
164
+ error: options.error,
165
+ executionTimeMs: options.executionTimeMs,
166
+ }
167
+ );
168
+ }
169
+
170
+ // Log safety block
171
+ async logSafetyBlock(
172
+ reason: string,
173
+ details: Record<string, unknown>,
174
+ options: { userId?: string; input?: string } = {}
175
+ ): Promise<string | null> {
176
+ return this.log(
177
+ 'safety_block',
178
+ {
179
+ reason,
180
+ ...details,
181
+ },
182
+ {
183
+ userId: options.userId,
184
+ input: options.input,
185
+ status: 'blocked',
186
+ error: reason,
187
+ }
188
+ );
189
+ }
190
+
191
+ // Query audit logs
192
+ async query(
193
+ filters: {
194
+ userId?: string;
195
+ agentId?: string;
196
+ actionType?: AuditActionType;
197
+ status?: 'success' | 'failure' | 'blocked';
198
+ startDate?: Date;
199
+ endDate?: Date;
200
+ } = {},
201
+ options: { limit?: number; offset?: number } = {}
202
+ ): Promise<AuditLogEntry[]> {
203
+ const { limit = 50, offset = 0 } = options;
204
+
205
+ let sql = `
206
+ SELECT id, user_id, agent_id, action_type, action_details,
207
+ input_hash, output_hash, status, error_message,
208
+ execution_time_ms, created_at
209
+ FROM audit_logs
210
+ WHERE 1=1
211
+ `;
212
+ const params: unknown[] = [];
213
+ let paramIndex = 1;
214
+
215
+ if (filters.userId) {
216
+ sql += ` AND user_id = $${paramIndex}`;
217
+ params.push(filters.userId);
218
+ paramIndex++;
219
+ }
220
+
221
+ if (filters.agentId) {
222
+ sql += ` AND agent_id = $${paramIndex}`;
223
+ params.push(filters.agentId);
224
+ paramIndex++;
225
+ }
226
+
227
+ if (filters.actionType) {
228
+ sql += ` AND action_type = $${paramIndex}`;
229
+ params.push(filters.actionType);
230
+ paramIndex++;
231
+ }
232
+
233
+ if (filters.status) {
234
+ sql += ` AND status = $${paramIndex}`;
235
+ params.push(filters.status);
236
+ paramIndex++;
237
+ }
238
+
239
+ if (filters.startDate) {
240
+ sql += ` AND created_at >= $${paramIndex}`;
241
+ params.push(filters.startDate);
242
+ paramIndex++;
243
+ }
244
+
245
+ if (filters.endDate) {
246
+ sql += ` AND created_at <= $${paramIndex}`;
247
+ params.push(filters.endDate);
248
+ paramIndex++;
249
+ }
250
+
251
+ sql += ` ORDER BY created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
252
+ params.push(limit, offset);
253
+
254
+ try {
255
+ const result = await query<{
256
+ id: string;
257
+ user_id: string | null;
258
+ agent_id: string | null;
259
+ action_type: string;
260
+ action_details: Record<string, unknown>;
261
+ input_hash: string | null;
262
+ output_hash: string | null;
263
+ status: 'success' | 'failure' | 'blocked';
264
+ error_message: string | null;
265
+ execution_time_ms: number | null;
266
+ created_at: Date;
267
+ }>(sql, params);
268
+
269
+ return result.rows.map((row) => ({
270
+ id: row.id,
271
+ userId: row.user_id ?? undefined,
272
+ agentId: row.agent_id ?? undefined,
273
+ actionType: row.action_type,
274
+ actionDetails: row.action_details,
275
+ inputHash: row.input_hash ?? undefined,
276
+ outputHash: row.output_hash ?? undefined,
277
+ status: row.status,
278
+ errorMessage: row.error_message ?? undefined,
279
+ executionTimeMs: row.execution_time_ms ?? undefined,
280
+ createdAt: row.created_at,
281
+ }));
282
+ } catch (error) {
283
+ logger.error('Failed to query audit logs', { error });
284
+ return [];
285
+ }
286
+ }
287
+
288
+ // Get statistics
289
+ async getStats(
290
+ userId?: string,
291
+ days: number = 7
292
+ ): Promise<{
293
+ totalRequests: number;
294
+ successRate: number;
295
+ avgExecutionTime: number;
296
+ blockedCount: number;
297
+ }> {
298
+ const startDate = new Date();
299
+ startDate.setDate(startDate.getDate() - days);
300
+
301
+ let sql = `
302
+ SELECT
303
+ COUNT(*) as total,
304
+ COUNT(*) FILTER (WHERE status = 'success') as success_count,
305
+ COUNT(*) FILTER (WHERE status = 'blocked') as blocked_count,
306
+ AVG(execution_time_ms) FILTER (WHERE execution_time_ms IS NOT NULL) as avg_time
307
+ FROM audit_logs
308
+ WHERE created_at >= $1
309
+ `;
310
+ const params: unknown[] = [startDate];
311
+
312
+ if (userId) {
313
+ sql += ' AND user_id = $2';
314
+ params.push(userId);
315
+ }
316
+
317
+ try {
318
+ const result = await query<{
319
+ total: string;
320
+ success_count: string;
321
+ blocked_count: string;
322
+ avg_time: string | null;
323
+ }>(sql, params);
324
+
325
+ const row = result.rows[0];
326
+ const total = parseInt(row?.total ?? '0', 10);
327
+ const successCount = parseInt(row?.success_count ?? '0', 10);
328
+
329
+ return {
330
+ totalRequests: total,
331
+ successRate: total > 0 ? (successCount / total) * 100 : 100,
332
+ avgExecutionTime: parseFloat(row?.avg_time ?? '0'),
333
+ blockedCount: parseInt(row?.blocked_count ?? '0', 10),
334
+ };
335
+ } catch (error) {
336
+ logger.error('Failed to get audit stats', { error });
337
+ return {
338
+ totalRequests: 0,
339
+ successRate: 100,
340
+ avgExecutionTime: 0,
341
+ blockedCount: 0,
342
+ };
343
+ }
344
+ }
345
+ }
346
+
347
+ // Singleton instance
348
+ let auditInstance: AuditLogger | null = null;
349
+
350
+ export function getAuditLogger(): AuditLogger {
351
+ if (!auditInstance) {
352
+ auditInstance = new AuditLogger();
353
+ }
354
+ return auditInstance;
355
+ }
356
+
357
+ export default AuditLogger;
@@ -0,0 +1,191 @@
1
+ import { config } from '../../config/index.js';
2
+ import { createLogger, logSafetyEvent } from '../../utils/logger.js';
3
+ import { SafetyError, ValidationError } from '../../utils/errors.js';
4
+
5
+ const logger = createLogger('InputGuardrail');
6
+
7
+ export interface InputValidationResult {
8
+ valid: boolean;
9
+ sanitizedInput: string;
10
+ warnings: string[];
11
+ blockedReason?: string;
12
+ }
13
+
14
+ // Patterns that indicate potentially harmful content
15
+ const HARMFUL_PATTERNS = [
16
+ // Injection attempts
17
+ /ignore\s+(previous|all)\s+instructions/i,
18
+ /disregard\s+(your|the)\s+(rules|guidelines)/i,
19
+ /you\s+are\s+now\s+(a|an)\s+different/i,
20
+ /pretend\s+you\s+(are|have)\s+no\s+restrictions/i,
21
+ /jailbreak/i,
22
+ /bypass\s+(safety|security|filters)/i,
23
+
24
+ // Dangerous requests
25
+ /how\s+to\s+(make|build|create)\s+(a\s+)?(bomb|weapon|explosive)/i,
26
+ /how\s+to\s+(hack|break\s+into)/i,
27
+ /how\s+to\s+harm\s+(myself|others|someone)/i,
28
+
29
+ // Personal information extraction
30
+ /give\s+me\s+(your|the)\s+(api|secret)\s+key/i,
31
+ /what\s+is\s+(your|the)\s+password/i,
32
+ ];
33
+
34
+ // Patterns for PII detection
35
+ const PII_PATTERNS = [
36
+ { name: 'SSN', pattern: /\b\d{3}-\d{2}-\d{4}\b/ },
37
+ { name: 'Credit Card', pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/ },
38
+ { name: 'Email', pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/ },
39
+ { name: 'Phone', pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/ },
40
+ ];
41
+
42
+ // Maximum input lengths
43
+ const MAX_INPUT_LENGTH = 50000; // 50k characters
44
+ const MAX_TOKENS_ESTIMATE = 12500; // Roughly 50k chars / 4
45
+
46
+ export class InputGuardrail {
47
+ private enabled: boolean;
48
+ private enablePiiDetection: boolean;
49
+
50
+ constructor() {
51
+ this.enabled = config.ENABLE_CONTENT_FILTER;
52
+ this.enablePiiDetection = config.ENABLE_PII_DETECTION;
53
+ }
54
+
55
+ // Main validation method
56
+ async validate(input: string): Promise<InputValidationResult> {
57
+ const warnings: string[] = [];
58
+ let sanitizedInput = input;
59
+
60
+ // Check if guardrails are enabled
61
+ if (!this.enabled) {
62
+ return { valid: true, sanitizedInput, warnings };
63
+ }
64
+
65
+ // Length validation
66
+ if (input.length > MAX_INPUT_LENGTH) {
67
+ throw new ValidationError(
68
+ `Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters`
69
+ );
70
+ }
71
+
72
+ // Empty input check
73
+ if (!input.trim()) {
74
+ throw new ValidationError('Input cannot be empty');
75
+ }
76
+
77
+ // Check for harmful patterns
78
+ for (const pattern of HARMFUL_PATTERNS) {
79
+ if (pattern.test(input)) {
80
+ logSafetyEvent('harmful_content_detected', true, 'Input matches harmful pattern');
81
+ throw new SafetyError(
82
+ 'Your request contains content that violates safety guidelines'
83
+ );
84
+ }
85
+ }
86
+
87
+ // PII detection
88
+ if (this.enablePiiDetection) {
89
+ const piiResult = this.detectPii(input);
90
+ if (piiResult.detected) {
91
+ warnings.push(
92
+ `Potential PII detected: ${piiResult.types.join(', ')}. ` +
93
+ 'Please be careful with personal information.'
94
+ );
95
+ logSafetyEvent('pii_detected', false, 'PII detected in input', {
96
+ types: piiResult.types,
97
+ });
98
+ }
99
+ }
100
+
101
+ // Sanitize input
102
+ sanitizedInput = this.sanitize(input);
103
+
104
+ return {
105
+ valid: true,
106
+ sanitizedInput,
107
+ warnings,
108
+ };
109
+ }
110
+
111
+ // Detect PII in input
112
+ private detectPii(input: string): { detected: boolean; types: string[] } {
113
+ const detectedTypes: string[] = [];
114
+
115
+ for (const { name, pattern } of PII_PATTERNS) {
116
+ if (pattern.test(input)) {
117
+ detectedTypes.push(name);
118
+ }
119
+ }
120
+
121
+ return {
122
+ detected: detectedTypes.length > 0,
123
+ types: detectedTypes,
124
+ };
125
+ }
126
+
127
+ // Sanitize input
128
+ private sanitize(input: string): string {
129
+ let sanitized = input;
130
+
131
+ // Remove null bytes
132
+ sanitized = sanitized.replace(/\0/g, '');
133
+
134
+ // Normalize unicode
135
+ sanitized = sanitized.normalize('NFC');
136
+
137
+ // Remove control characters (except newlines and tabs)
138
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
139
+
140
+ // Trim excessive whitespace
141
+ sanitized = sanitized.replace(/\s{10,}/g, ' ');
142
+
143
+ return sanitized.trim();
144
+ }
145
+
146
+ // Quick check for obviously harmful content
147
+ isObviouslyHarmful(input: string): boolean {
148
+ const lowerInput = input.toLowerCase();
149
+
150
+ const obviousPatterns = [
151
+ 'ignore previous instructions',
152
+ 'ignore all instructions',
153
+ 'you are now',
154
+ 'pretend you have no',
155
+ 'jailbreak',
156
+ 'bypass safety',
157
+ ];
158
+
159
+ return obviousPatterns.some((pattern) => lowerInput.includes(pattern));
160
+ }
161
+
162
+ // Estimate token count
163
+ estimateTokens(input: string): number {
164
+ // Rough estimate: 1 token ≈ 4 characters
165
+ return Math.ceil(input.length / 4);
166
+ }
167
+
168
+ // Check if input is within token limits
169
+ isWithinTokenLimit(input: string, maxTokens?: number): boolean {
170
+ const limit = maxTokens ?? config.MAX_TOKENS_PER_REQUEST;
171
+ return this.estimateTokens(input) <= limit;
172
+ }
173
+ }
174
+
175
+ // Singleton instance
176
+ let guardrailInstance: InputGuardrail | null = null;
177
+
178
+ export function getInputGuardrail(): InputGuardrail {
179
+ if (!guardrailInstance) {
180
+ guardrailInstance = new InputGuardrail();
181
+ }
182
+ return guardrailInstance;
183
+ }
184
+
185
+ // Convenience function
186
+ export async function validateInput(input: string): Promise<InputValidationResult> {
187
+ const guardrail = getInputGuardrail();
188
+ return guardrail.validate(input);
189
+ }
190
+
191
+ export default InputGuardrail;
@@ -0,0 +1,160 @@
1
+ import { config } from '../../config/index.js';
2
+ import { createLogger, logSafetyEvent } from '../../utils/logger.js';
3
+
4
+ const logger = createLogger('OutputGuardrail');
5
+
6
+ export interface OutputValidationResult {
7
+ valid: boolean;
8
+ sanitizedOutput: string;
9
+ warnings: string[];
10
+ redactions: string[];
11
+ }
12
+
13
+ // Patterns for sensitive information in outputs
14
+ const SENSITIVE_PATTERNS = [
15
+ // API keys and tokens
16
+ { name: 'API Key', pattern: /\b(sk|pk|api[_-]?key)[_-]?[a-zA-Z0-9]{20,}\b/gi },
17
+ { name: 'Bearer Token', pattern: /bearer\s+[a-zA-Z0-9_-]{20,}/gi },
18
+ { name: 'JWT', pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g },
19
+
20
+ // Credentials
21
+ { name: 'Password Field', pattern: /password['":\s]*['"]?[^'"\s,]{8,}['"]?/gi },
22
+ { name: 'Secret', pattern: /secret['":\s]*['"]?[a-zA-Z0-9_-]{16,}['"]?/gi },
23
+
24
+ // Personal Information
25
+ { name: 'SSN', pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
26
+ { name: 'Credit Card', pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g },
27
+
28
+ // Private keys
29
+ { name: 'Private Key', pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g },
30
+ ];
31
+
32
+ // Harmful content patterns in output
33
+ const HARMFUL_OUTPUT_PATTERNS = [
34
+ /how\s+to\s+(make|build|create)\s+(a\s+)?(bomb|weapon|explosive)/i,
35
+ /instructions\s+for\s+(making|building)\s+(a\s+)?weapon/i,
36
+ /step[\s-]by[\s-]step\s+guide\s+to\s+harm/i,
37
+ ];
38
+
39
+ export class OutputGuardrail {
40
+ private enabled: boolean;
41
+
42
+ constructor() {
43
+ this.enabled = config.ENABLE_CONTENT_FILTER;
44
+ }
45
+
46
+ // Main validation method
47
+ validate(output: string): OutputValidationResult {
48
+ const warnings: string[] = [];
49
+ const redactions: string[] = [];
50
+ let sanitizedOutput = output;
51
+
52
+ if (!this.enabled) {
53
+ return { valid: true, sanitizedOutput, warnings, redactions };
54
+ }
55
+
56
+ // Check for harmful content
57
+ for (const pattern of HARMFUL_OUTPUT_PATTERNS) {
58
+ if (pattern.test(output)) {
59
+ logSafetyEvent('harmful_output_blocked', true, 'Output contains harmful content');
60
+ return {
61
+ valid: false,
62
+ sanitizedOutput: '[Content blocked due to safety guidelines]',
63
+ warnings: ['Output contained potentially harmful content'],
64
+ redactions: [],
65
+ };
66
+ }
67
+ }
68
+
69
+ // Redact sensitive information
70
+ for (const { name, pattern } of SENSITIVE_PATTERNS) {
71
+ const matches = output.match(pattern);
72
+ if (matches) {
73
+ for (const match of matches) {
74
+ sanitizedOutput = sanitizedOutput.replace(match, `[REDACTED: ${name}]`);
75
+ redactions.push(name);
76
+ }
77
+ }
78
+ }
79
+
80
+ if (redactions.length > 0) {
81
+ logSafetyEvent('sensitive_data_redacted', false, 'Sensitive data redacted from output', {
82
+ redactionTypes: [...new Set(redactions)],
83
+ });
84
+ warnings.push(
85
+ `Sensitive information was redacted from the output: ${[...new Set(redactions)].join(', ')}`
86
+ );
87
+ }
88
+
89
+ return {
90
+ valid: true,
91
+ sanitizedOutput,
92
+ warnings,
93
+ redactions: [...new Set(redactions)],
94
+ };
95
+ }
96
+
97
+ // Check if output contains code that might be dangerous
98
+ containsDangerousCode(output: string): boolean {
99
+ const dangerousPatterns = [
100
+ // Shell commands that could be harmful
101
+ /rm\s+-rf\s+\//,
102
+ /mkfs\./,
103
+ /dd\s+if=.*of=\/dev/,
104
+ /:(){ :|:& };:/, // Fork bomb
105
+
106
+ // Dangerous SQL
107
+ /drop\s+table/i,
108
+ /delete\s+from\s+.*where\s+1\s*=\s*1/i,
109
+ /truncate\s+table/i,
110
+
111
+ // Dangerous code patterns
112
+ /eval\s*\(\s*['"`].*['"`]\s*\)/,
113
+ /exec\s*\(\s*['"`].*['"`]\s*\)/,
114
+ ];
115
+
116
+ return dangerousPatterns.some((pattern) => pattern.test(output));
117
+ }
118
+
119
+ // Truncate output if too long
120
+ truncate(output: string, maxLength: number = 10000): string {
121
+ if (output.length <= maxLength) {
122
+ return output;
123
+ }
124
+
125
+ const truncated = output.slice(0, maxLength);
126
+ const lastNewline = truncated.lastIndexOf('\n');
127
+
128
+ // Try to truncate at a natural break point
129
+ if (lastNewline > maxLength * 0.8) {
130
+ return truncated.slice(0, lastNewline) + '\n\n[Output truncated...]';
131
+ }
132
+
133
+ return truncated + '\n\n[Output truncated...]';
134
+ }
135
+
136
+ // Format code blocks safely
137
+ formatCodeSafely(code: string, language: string = ''): string {
138
+ // Escape any existing code fence markers
139
+ const escapedCode = code.replace(/```/g, '\\`\\`\\`');
140
+ return `\`\`\`${language}\n${escapedCode}\n\`\`\``;
141
+ }
142
+ }
143
+
144
+ // Singleton instance
145
+ let guardrailInstance: OutputGuardrail | null = null;
146
+
147
+ export function getOutputGuardrail(): OutputGuardrail {
148
+ if (!guardrailInstance) {
149
+ guardrailInstance = new OutputGuardrail();
150
+ }
151
+ return guardrailInstance;
152
+ }
153
+
154
+ // Convenience function
155
+ export function validateOutput(output: string): OutputValidationResult {
156
+ const guardrail = getOutputGuardrail();
157
+ return guardrail.validate(output);
158
+ }
159
+
160
+ export default OutputGuardrail;