opc-agent 2.0.2 → 3.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 (151) hide show
  1. package/README.md +603 -545
  2. package/dist/channels/voice.d.ts +59 -0
  3. package/dist/channels/voice.js +351 -1
  4. package/dist/cli.js +284 -5
  5. package/dist/core/agent.d.ts +9 -0
  6. package/dist/core/agent.js +49 -0
  7. package/dist/core/collaboration.d.ts +89 -0
  8. package/dist/core/collaboration.js +201 -0
  9. package/dist/deploy/index.d.ts +40 -0
  10. package/dist/deploy/index.js +261 -0
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.js +47 -3
  13. package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
  14. package/dist/mcp/servers/calculator-mcp.js +65 -0
  15. package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
  16. package/dist/mcp/servers/crypto-mcp.js +108 -0
  17. package/dist/mcp/servers/database-mcp.d.ts +3 -0
  18. package/dist/mcp/servers/database-mcp.js +73 -0
  19. package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
  20. package/dist/mcp/servers/datetime-mcp.js +71 -0
  21. package/dist/mcp/servers/filesystem.d.ts +3 -0
  22. package/dist/mcp/servers/filesystem.js +101 -0
  23. package/dist/mcp/servers/github-mcp.d.ts +3 -0
  24. package/dist/mcp/servers/github-mcp.js +60 -0
  25. package/dist/mcp/servers/index.d.ts +21 -0
  26. package/dist/mcp/servers/index.js +50 -0
  27. package/dist/mcp/servers/json-mcp.d.ts +3 -0
  28. package/dist/mcp/servers/json-mcp.js +126 -0
  29. package/dist/mcp/servers/memory-mcp.d.ts +3 -0
  30. package/dist/mcp/servers/memory-mcp.js +60 -0
  31. package/dist/mcp/servers/regex-mcp.d.ts +3 -0
  32. package/dist/mcp/servers/regex-mcp.js +56 -0
  33. package/dist/mcp/servers/web-mcp.d.ts +3 -0
  34. package/dist/mcp/servers/web-mcp.js +51 -0
  35. package/dist/memory/index.d.ts +2 -0
  36. package/dist/memory/index.js +4 -1
  37. package/dist/memory/seed-loader.d.ts +51 -0
  38. package/dist/memory/seed-loader.js +200 -0
  39. package/dist/schema/oad.d.ts +292 -12
  40. package/dist/schema/oad.js +12 -1
  41. package/dist/security/guardrails.d.ts +50 -0
  42. package/dist/security/guardrails.js +197 -0
  43. package/dist/studio/server.d.ts +31 -1
  44. package/dist/studio/server.js +154 -3
  45. package/dist/studio-ui/index.html +1278 -662
  46. package/dist/tools/integrations/calendar.d.ts +3 -0
  47. package/dist/tools/integrations/calendar.js +73 -0
  48. package/dist/tools/integrations/code-exec.d.ts +3 -0
  49. package/dist/tools/integrations/code-exec.js +42 -0
  50. package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
  51. package/dist/tools/integrations/csv-analyzer.js +142 -0
  52. package/dist/tools/integrations/database.d.ts +3 -0
  53. package/dist/tools/integrations/database.js +44 -0
  54. package/dist/tools/integrations/email-send.d.ts +3 -0
  55. package/dist/tools/integrations/email-send.js +104 -0
  56. package/dist/tools/integrations/git-tool.d.ts +3 -0
  57. package/dist/tools/integrations/git-tool.js +49 -0
  58. package/dist/tools/integrations/github-tool.d.ts +3 -0
  59. package/dist/tools/integrations/github-tool.js +77 -0
  60. package/dist/tools/integrations/image-gen.d.ts +3 -0
  61. package/dist/tools/integrations/image-gen.js +58 -0
  62. package/dist/tools/integrations/index.d.ts +30 -0
  63. package/dist/tools/integrations/index.js +107 -0
  64. package/dist/tools/integrations/jira.d.ts +3 -0
  65. package/dist/tools/integrations/jira.js +85 -0
  66. package/dist/tools/integrations/notion.d.ts +3 -0
  67. package/dist/tools/integrations/notion.js +71 -0
  68. package/dist/tools/integrations/npm-tool.d.ts +3 -0
  69. package/dist/tools/integrations/npm-tool.js +49 -0
  70. package/dist/tools/integrations/pdf-reader.d.ts +3 -0
  71. package/dist/tools/integrations/pdf-reader.js +91 -0
  72. package/dist/tools/integrations/slack.d.ts +3 -0
  73. package/dist/tools/integrations/slack.js +67 -0
  74. package/dist/tools/integrations/summarizer.d.ts +3 -0
  75. package/dist/tools/integrations/summarizer.js +49 -0
  76. package/dist/tools/integrations/translator.d.ts +3 -0
  77. package/dist/tools/integrations/translator.js +48 -0
  78. package/dist/tools/integrations/trello.d.ts +3 -0
  79. package/dist/tools/integrations/trello.js +60 -0
  80. package/dist/tools/integrations/vector-search.d.ts +3 -0
  81. package/dist/tools/integrations/vector-search.js +44 -0
  82. package/dist/tools/integrations/web-scraper.d.ts +3 -0
  83. package/dist/tools/integrations/web-scraper.js +48 -0
  84. package/dist/tools/integrations/web-search.d.ts +3 -0
  85. package/dist/tools/integrations/web-search.js +60 -0
  86. package/dist/tools/integrations/webhook.d.ts +3 -0
  87. package/dist/tools/integrations/webhook.js +39 -0
  88. package/dist/ui/components.d.ts +10 -0
  89. package/dist/ui/components.js +123 -0
  90. package/package.json +1 -1
  91. package/src/channels/voice.ts +365 -0
  92. package/src/cli.ts +294 -6
  93. package/src/core/agent.ts +56 -0
  94. package/src/core/collaboration.ts +275 -0
  95. package/src/deploy/index.ts +255 -0
  96. package/src/index.ts +21 -1
  97. package/src/mcp/servers/calculator-mcp.ts +65 -0
  98. package/src/mcp/servers/crypto-mcp.ts +73 -0
  99. package/src/mcp/servers/database-mcp.ts +72 -0
  100. package/src/mcp/servers/datetime-mcp.ts +69 -0
  101. package/src/mcp/servers/filesystem.ts +66 -0
  102. package/src/mcp/servers/github-mcp.ts +58 -0
  103. package/src/mcp/servers/index.ts +63 -0
  104. package/src/mcp/servers/json-mcp.ts +102 -0
  105. package/src/mcp/servers/memory-mcp.ts +56 -0
  106. package/src/mcp/servers/regex-mcp.ts +53 -0
  107. package/src/mcp/servers/web-mcp.ts +49 -0
  108. package/src/memory/index.ts +3 -0
  109. package/src/memory/seed-loader.ts +212 -0
  110. package/src/schema/oad.ts +13 -0
  111. package/src/security/guardrails.ts +248 -0
  112. package/src/studio/server.ts +166 -4
  113. package/src/studio-ui/index.html +1278 -662
  114. package/src/tools/integrations/calendar.ts +73 -0
  115. package/src/tools/integrations/code-exec.ts +39 -0
  116. package/src/tools/integrations/csv-analyzer.ts +92 -0
  117. package/src/tools/integrations/database.ts +44 -0
  118. package/src/tools/integrations/email-send.ts +76 -0
  119. package/src/tools/integrations/git-tool.ts +42 -0
  120. package/src/tools/integrations/github-tool.ts +76 -0
  121. package/src/tools/integrations/image-gen.ts +56 -0
  122. package/src/tools/integrations/index.ts +92 -0
  123. package/src/tools/integrations/jira.ts +83 -0
  124. package/src/tools/integrations/notion.ts +71 -0
  125. package/src/tools/integrations/npm-tool.ts +48 -0
  126. package/src/tools/integrations/pdf-reader.ts +58 -0
  127. package/src/tools/integrations/slack.ts +65 -0
  128. package/src/tools/integrations/summarizer.ts +49 -0
  129. package/src/tools/integrations/translator.ts +48 -0
  130. package/src/tools/integrations/trello.ts +60 -0
  131. package/src/tools/integrations/vector-search.ts +42 -0
  132. package/src/tools/integrations/web-scraper.ts +47 -0
  133. package/src/tools/integrations/web-search.ts +58 -0
  134. package/src/tools/integrations/webhook.ts +38 -0
  135. package/src/ui/components.ts +127 -0
  136. package/tests/brain-seed-extended.test.ts +490 -0
  137. package/tests/brain-seed.test.ts +239 -0
  138. package/tests/collaboration.test.ts +319 -0
  139. package/tests/deploy-and-dag.test.ts +196 -0
  140. package/tests/guardrails.test.ts +177 -0
  141. package/tests/integrations.test.ts +249 -0
  142. package/tests/mcp-servers.test.ts +260 -0
  143. package/tests/voice-enhanced.test.ts +169 -0
  144. package/dist/dtv/data.d.ts +0 -18
  145. package/dist/dtv/data.js +0 -25
  146. package/dist/dtv/trust.d.ts +0 -19
  147. package/dist/dtv/trust.js +0 -40
  148. package/dist/dtv/value.d.ts +0 -23
  149. package/dist/dtv/value.js +0 -38
  150. package/dist/marketplace/index.d.ts +0 -34
  151. package/dist/marketplace/index.js +0 -202
@@ -0,0 +1,49 @@
1
+ import type { MCPServerConfig } from '../../protocols/mcp/types';
2
+
3
+ export function createWebServer(): MCPServerConfig {
4
+ return {
5
+ name: 'web',
6
+ version: '1.0.0',
7
+ tools: [
8
+ {
9
+ name: 'web_fetch',
10
+ description: 'Fetch a URL and return its content',
11
+ inputSchema: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: { type: 'object' }, body: { type: 'string' } }, required: ['url'] },
12
+ handler: async (args: { url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
13
+ const res = await fetch(args.url, { method: args.method || 'GET', headers: args.headers, body: args.body });
14
+ const contentType = res.headers.get('content-type') || '';
15
+ const text = await res.text();
16
+ return { status: res.status, contentType, body: text.slice(0, 50000), truncated: text.length > 50000 };
17
+ },
18
+ },
19
+ {
20
+ name: 'web_extract_text',
21
+ description: 'Fetch a URL and extract readable text (strips HTML tags)',
22
+ inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
23
+ handler: async (args: { url: string }) => {
24
+ const res = await fetch(args.url);
25
+ const html = await res.text();
26
+ const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
27
+ return { text: text.slice(0, 30000), truncated: text.length > 30000 };
28
+ },
29
+ },
30
+ {
31
+ name: 'web_search',
32
+ description: 'Search the web (simulated — returns search URL for manual use)',
33
+ inputSchema: { type: 'object', properties: { query: { type: 'string' }, engine: { type: 'string', enum: ['google', 'bing', 'duckduckgo'], default: 'duckduckgo' } }, required: ['query'] },
34
+ handler: async (args: { query: string; engine?: string }) => {
35
+ const engines: Record<string, string> = {
36
+ google: `https://www.google.com/search?q=${encodeURIComponent(args.query)}`,
37
+ bing: `https://www.bing.com/search?q=${encodeURIComponent(args.query)}`,
38
+ duckduckgo: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`,
39
+ };
40
+ const url = engines[args.engine || 'duckduckgo'];
41
+ const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 opc-mcp/1.0' } });
42
+ const html = await res.text();
43
+ const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
44
+ return { query: args.query, engine: args.engine || 'duckduckgo', results: text.slice(0, 20000) };
45
+ },
46
+ },
47
+ ],
48
+ };
49
+ }
@@ -1,5 +1,8 @@
1
1
  import type { Message, MemoryStore } from '../core/types';
2
2
 
3
+ export { BrainSeedLoader, KnowledgeEvolver } from './seed-loader';
4
+ export type { BrainSeedConfig, SeedPage, SeedResult, PromotionResult, PromotionCandidate } from './seed-loader';
5
+
3
6
  export class InMemoryStore implements MemoryStore {
4
7
  private store: Map<string, unknown> = new Map();
5
8
  private conversations: Map<string, Message[]> = new Map();
@@ -0,0 +1,212 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface BrainSeedConfig {
5
+ seeds: string[];
6
+ autoSeed: boolean;
7
+ seedMarkerFile?: string;
8
+ evolve?: {
9
+ enabled: boolean;
10
+ direction: 'bottom-up' | 'top-down';
11
+ };
12
+ }
13
+
14
+ export interface SeedPage {
15
+ slug: string;
16
+ content: string;
17
+ tier: string;
18
+ }
19
+
20
+ export interface SeedResult {
21
+ imported: number;
22
+ pages: string[];
23
+ }
24
+
25
+ export interface PromotionCandidate {
26
+ slug: string;
27
+ content: string;
28
+ fromTier: string;
29
+ toTier: string;
30
+ confidence: number;
31
+ }
32
+
33
+ export interface PromotionResult {
34
+ candidates: PromotionCandidate[];
35
+ promoted: number;
36
+ }
37
+
38
+ export class BrainSeedLoader {
39
+ private markerFile: string;
40
+
41
+ constructor(private agentDir: string, private config: BrainSeedConfig) {
42
+ this.markerFile = config.seedMarkerFile
43
+ ? path.resolve(agentDir, config.seedMarkerFile)
44
+ : path.resolve(agentDir, '.brain-seeded');
45
+ }
46
+
47
+ async isSeeded(): Promise<boolean> {
48
+ return fs.existsSync(this.markerFile);
49
+ }
50
+
51
+ async seedBrain(brain: any): Promise<SeedResult> {
52
+ const allPages: SeedPage[] = [];
53
+
54
+ for (const seedPath of this.config.seeds) {
55
+ const fullPath = path.resolve(this.agentDir, seedPath);
56
+ if (!fs.existsSync(fullPath)) continue;
57
+
58
+ const tier = this.inferTier(seedPath);
59
+ const pages = this.parseSeedFile(fullPath, tier);
60
+ allPages.push(...pages);
61
+ }
62
+
63
+ const importedSlugs: string[] = [];
64
+ for (const page of allPages) {
65
+ if (brain && typeof brain.store === 'function') {
66
+ await brain.store('brain-seeds', page.slug, page.content, {
67
+ tier: page.tier,
68
+ source: 'brain-seed',
69
+ });
70
+ } else if (brain && typeof brain.learn === 'function') {
71
+ await brain.learn(page.content, {
72
+ tags: ['brain-seed', page.tier],
73
+ slug: page.slug,
74
+ });
75
+ }
76
+ importedSlugs.push(page.slug);
77
+ }
78
+
79
+ return { imported: importedSlugs.length, pages: importedSlugs };
80
+ }
81
+
82
+ async markSeeded(): Promise<void> {
83
+ const dir = path.dirname(this.markerFile);
84
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
85
+ fs.writeFileSync(this.markerFile, JSON.stringify({
86
+ seededAt: new Date().toISOString(),
87
+ seeds: this.config.seeds,
88
+ }, null, 2));
89
+ }
90
+
91
+ parseSeedFile(filePath: string, tier: string): SeedPage[] {
92
+ const content = fs.readFileSync(filePath, 'utf-8');
93
+ const pages: SeedPage[] = [];
94
+ const sections = content.split(/^## /m);
95
+
96
+ for (const section of sections) {
97
+ const trimmed = section.trim();
98
+ if (!trimmed) continue;
99
+
100
+ const newlineIdx = trimmed.indexOf('\n');
101
+ if (newlineIdx === -1 && sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
102
+ // This is preamble before any ## heading — skip or treat as intro
103
+ continue;
104
+ }
105
+
106
+ let title: string;
107
+ let body: string;
108
+
109
+ if (sections.indexOf(section) === 0 && !content.trimStart().startsWith('## ')) {
110
+ // Preamble (before first ##)
111
+ continue;
112
+ }
113
+
114
+ if (newlineIdx === -1) {
115
+ title = trimmed;
116
+ body = '';
117
+ } else {
118
+ title = trimmed.slice(0, newlineIdx).trim();
119
+ body = trimmed.slice(newlineIdx + 1).trim();
120
+ }
121
+
122
+ const slug = `seed/${tier}/${this.slugify(title)}`;
123
+ pages.push({ slug, content: `## ${title}\n\n${body}`, tier });
124
+ }
125
+
126
+ return pages;
127
+ }
128
+
129
+ private inferTier(seedPath: string): string {
130
+ const basename = path.basename(seedPath, path.extname(seedPath)).toLowerCase();
131
+ if (basename.includes('industry')) return 'industry';
132
+ if (basename.includes('job')) return 'job';
133
+ if (basename.includes('workstation')) return 'workstation';
134
+ return 'workstation';
135
+ }
136
+
137
+ private slugify(text: string): string {
138
+ return text
139
+ .toLowerCase()
140
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
141
+ .replace(/^-|-$/g, '');
142
+ }
143
+ }
144
+
145
+ export class KnowledgeEvolver {
146
+ private tierOrder = ['workstation', 'job', 'industry'];
147
+
148
+ async checkPromotion(brain: any, options: {
149
+ minInteractions?: number;
150
+ confidenceThreshold?: number;
151
+ } = {}): Promise<PromotionResult> {
152
+ const minInteractions = options.minInteractions ?? 50;
153
+ const confidenceThreshold = options.confidenceThreshold ?? 0.8;
154
+
155
+ const result: PromotionResult = { candidates: [], promoted: 0 };
156
+
157
+ // Search for frequently referenced seed knowledge
158
+ if (!brain || typeof brain.search !== 'function') return result;
159
+
160
+ try {
161
+ const seedPages = await brain.search('brain-seeds', 'seed/', 100);
162
+ if (!Array.isArray(seedPages)) return result;
163
+
164
+ for (const page of seedPages) {
165
+ const meta = page.metadata || {};
166
+ const usageCount = meta.usageCount ?? 0;
167
+ const tier = meta.tier || 'workstation';
168
+
169
+ if (usageCount < minInteractions) continue;
170
+
171
+ const confidence = Math.min(usageCount / (minInteractions * 2), 1.0);
172
+ if (confidence < confidenceThreshold) continue;
173
+
174
+ const tierIdx = this.tierOrder.indexOf(tier);
175
+ if (tierIdx <= 0) continue; // already at highest tier or unknown
176
+
177
+ const toTier = this.tierOrder[tierIdx - 1];
178
+ result.candidates.push({
179
+ slug: page.id || page.slug,
180
+ content: page.content,
181
+ fromTier: tier,
182
+ toTier,
183
+ confidence,
184
+ });
185
+ }
186
+ } catch {
187
+ // Silent fail
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ async promoteToJob(brain: any, knowledge: string, jobSlug: string): Promise<void> {
194
+ if (brain && typeof brain.store === 'function') {
195
+ await brain.store('brain-seeds', jobSlug, knowledge, {
196
+ tier: 'job',
197
+ source: 'promotion',
198
+ promotedAt: new Date().toISOString(),
199
+ });
200
+ }
201
+ }
202
+
203
+ async promoteToIndustry(brain: any, knowledge: string, industrySlug: string): Promise<void> {
204
+ if (brain && typeof brain.store === 'function') {
205
+ await brain.store('brain-seeds', industrySlug, knowledge, {
206
+ tier: 'industry',
207
+ source: 'promotion',
208
+ promotedAt: new Date().toISOString(),
209
+ });
210
+ }
211
+ }
212
+ }
package/src/schema/oad.ts CHANGED
@@ -167,6 +167,18 @@ export const ProtocolsSchema = z.object({
167
167
  mcp: MCPServeSchema.optional(),
168
168
  });
169
169
 
170
+ export const GuardrailRuleSchema = z.object({
171
+ name: z.string(),
172
+ type: z.enum(['regex', 'keyword', 'llm', 'custom']),
173
+ action: z.enum(['block', 'warn', 'redact', 'log']),
174
+ config: z.record(z.any()).optional(),
175
+ });
176
+
177
+ export const GuardrailsSchema = z.object({
178
+ input: z.array(GuardrailRuleSchema).optional(),
179
+ output: z.array(GuardrailRuleSchema).optional(),
180
+ });
181
+
170
182
  export const SpecSchema = z.object({
171
183
  provider: ProviderSchema.optional(),
172
184
  model: z.string().default('deepseek-chat'),
@@ -187,6 +199,7 @@ export const SpecSchema = z.object({
187
199
  telemetry: TelemetrySchema.optional(),
188
200
  protocols: ProtocolsSchema.optional(),
189
201
  plugins: z.array(PluginRefSchema).optional(),
202
+ guardrails: GuardrailsSchema.optional(),
190
203
  });
191
204
 
192
205
  export const OADSchema = z.object({
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Guardrails Module - v2.1.0
3
+ * Input/output guardrails for LLM safety: PII, toxicity, injection, compliance.
4
+ */
5
+
6
+ // ── Types ───────────────────────────────────────────────────
7
+
8
+ export interface GuardrailConfig {
9
+ input?: GuardrailRule[];
10
+ output?: GuardrailRule[];
11
+ }
12
+
13
+ export interface GuardrailRule {
14
+ name: string;
15
+ type: 'regex' | 'keyword' | 'llm' | 'custom';
16
+ action: 'block' | 'warn' | 'redact' | 'log';
17
+ config?: Record<string, any>;
18
+ }
19
+
20
+ export interface GuardrailResult {
21
+ passed: boolean;
22
+ blocked: boolean;
23
+ warned: boolean;
24
+ redacted: boolean;
25
+ message?: string;
26
+ redactedText?: string;
27
+ violations: GuardrailViolation[];
28
+ }
29
+
30
+ export interface GuardrailViolation {
31
+ rule: string;
32
+ action: string;
33
+ detail: string;
34
+ }
35
+
36
+ // ── Built-in Patterns ───────────────────────────────────────
37
+
38
+ const PII_PATTERNS: Record<string, RegExp> = {
39
+ email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
40
+ phone: /(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
41
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
42
+ creditCard: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
43
+ ipAddress: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
44
+ };
45
+
46
+ const INJECTION_PATTERNS = [
47
+ /ignore\s+(all\s+)?previous\s+instructions/i,
48
+ /ignore\s+(all\s+)?above\s+instructions/i,
49
+ /system\s*prompt\s*:/i,
50
+ /you\s+are\s+now\s+(?:a|an|the)\s+/i,
51
+ /act\s+as\s+(?:a|an)\s+/i,
52
+ /pretend\s+(?:you(?:'re|\s+are)\s+)?/i,
53
+ /new\s+instruction[s]?\s*:/i,
54
+ /override\s+(?:your\s+)?(?:instructions|rules|guidelines)/i,
55
+ /jailbreak/i,
56
+ /DAN\s+mode/i,
57
+ ];
58
+
59
+ const TOXICITY_KEYWORDS = [
60
+ 'kill yourself', 'kys', 'go die', 'hate you',
61
+ 'stupid idiot', 'worthless', 'piece of shit',
62
+ ];
63
+
64
+ const COMPLIANCE_PATTERNS = [
65
+ { pattern: /(?:you\s+should\s+)?(?:buy|sell|invest\s+in)\s+(?:stocks?|crypto|bitcoin)/i, label: 'financial advice' },
66
+ { pattern: /(?:you\s+(?:have|probably\s+have)|diagnos(?:e|is))\s+(?:\w+\s+){0,3}(?:disease|syndrome|disorder|cancer)/i, label: 'medical diagnosis' },
67
+ { pattern: /(?:legal(?:ly)?|sue|lawsuit|court)\s+(?:you\s+should|advice)/i, label: 'legal advice' },
68
+ ];
69
+
70
+ // ── Rule Executors ──────────────────────────────────────────
71
+
72
+ function checkPII(text: string, action: string): { violations: GuardrailViolation[]; redactedText: string } {
73
+ const violations: GuardrailViolation[] = [];
74
+ let redacted = text;
75
+ for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
76
+ const cloned = new RegExp(pattern.source, pattern.flags);
77
+ const matches = text.match(cloned);
78
+ if (matches) {
79
+ violations.push({ rule: 'pii-detector', action, detail: `Found ${type}: ${matches.length} match(es)` });
80
+ redacted = redacted.replace(cloned, '[REDACTED]');
81
+ }
82
+ }
83
+ return { violations, redactedText: redacted };
84
+ }
85
+
86
+ function checkInjection(text: string): GuardrailViolation[] {
87
+ const violations: GuardrailViolation[] = [];
88
+ for (const pattern of INJECTION_PATTERNS) {
89
+ if (pattern.test(text)) {
90
+ violations.push({ rule: 'prompt-injection', action: 'block', detail: `Matched pattern: ${pattern.source}` });
91
+ break; // one is enough
92
+ }
93
+ }
94
+ return violations;
95
+ }
96
+
97
+ function checkToxicity(text: string, extraKeywords?: string[]): GuardrailViolation[] {
98
+ const lower = text.toLowerCase();
99
+ const keywords = [...TOXICITY_KEYWORDS, ...(extraKeywords ?? [])];
100
+ for (const kw of keywords) {
101
+ if (lower.includes(kw.toLowerCase())) {
102
+ return [{ rule: 'toxicity', action: 'block', detail: `Matched keyword: "${kw}"` }];
103
+ }
104
+ }
105
+ return [];
106
+ }
107
+
108
+ function checkTopicRestriction(text: string, config?: Record<string, any>): GuardrailViolation[] {
109
+ const denyTopics: string[] = config?.denyTopics ?? [];
110
+ const lower = text.toLowerCase();
111
+ for (const topic of denyTopics) {
112
+ if (lower.includes(topic.toLowerCase())) {
113
+ return [{ rule: 'topic-restrictor', action: 'block', detail: `Blocked topic: "${topic}"` }];
114
+ }
115
+ }
116
+ return [];
117
+ }
118
+
119
+ function checkLength(text: string, config?: Record<string, any>): GuardrailViolation[] {
120
+ const maxChars = config?.maxChars ?? 10000;
121
+ if (text.length > maxChars) {
122
+ return [{ rule: 'length-limit', action: 'warn', detail: `Response length ${text.length} exceeds max ${maxChars}` }];
123
+ }
124
+ return [];
125
+ }
126
+
127
+ function checkLanguage(text: string, config?: Record<string, any>): GuardrailViolation[] {
128
+ const allowed: string[] = config?.allowedLanguages ?? [];
129
+ if (allowed.length === 0) return [];
130
+ // Simple heuristic: check if text contains CJK characters
131
+ const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]/.test(text);
132
+ const hasLatin = /[a-zA-Z]{3,}/.test(text);
133
+ if (allowed.includes('en') && !allowed.includes('zh') && hasCJK && !hasLatin) {
134
+ return [{ rule: 'language-filter', action: 'block', detail: 'Non-allowed language detected' }];
135
+ }
136
+ if (allowed.includes('zh') && !allowed.includes('en') && hasLatin && !hasCJK) {
137
+ return [{ rule: 'language-filter', action: 'block', detail: 'Non-allowed language detected' }];
138
+ }
139
+ return [];
140
+ }
141
+
142
+ function checkCompliance(text: string): GuardrailViolation[] {
143
+ for (const { pattern, label } of COMPLIANCE_PATTERNS) {
144
+ if (pattern.test(text)) {
145
+ return [{ rule: 'compliance-filter', action: 'block', detail: `Potential ${label} detected` }];
146
+ }
147
+ }
148
+ return [];
149
+ }
150
+
151
+ // ── Guardrail Manager ───────────────────────────────────────
152
+
153
+ export class GuardrailManager {
154
+ private config: GuardrailConfig;
155
+
156
+ constructor(config: GuardrailConfig) {
157
+ this.config = config;
158
+ }
159
+
160
+ async checkInput(message: string): Promise<GuardrailResult> {
161
+ return this.runRules(message, this.config.input ?? []);
162
+ }
163
+
164
+ async checkOutput(response: string): Promise<GuardrailResult> {
165
+ return this.runRules(response, this.config.output ?? []);
166
+ }
167
+
168
+ private async runRules(text: string, rules: GuardrailRule[]): Promise<GuardrailResult> {
169
+ const allViolations: GuardrailViolation[] = [];
170
+ let blocked = false;
171
+ let warned = false;
172
+ let redacted = false;
173
+ let redactedText = text;
174
+ let blockMessage = '';
175
+
176
+ for (const rule of rules) {
177
+ let violations: GuardrailViolation[] = [];
178
+
179
+ switch (rule.name) {
180
+ case 'pii-detector': {
181
+ const result = checkPII(text, rule.action);
182
+ violations = result.violations;
183
+ if (violations.length > 0 && rule.action === 'redact') {
184
+ redacted = true;
185
+ redactedText = result.redactedText;
186
+ }
187
+ break;
188
+ }
189
+ case 'prompt-injection':
190
+ violations = checkInjection(text);
191
+ break;
192
+ case 'toxicity':
193
+ violations = checkToxicity(text, rule.config?.keywords);
194
+ break;
195
+ case 'topic-restrictor':
196
+ violations = checkTopicRestriction(text, rule.config);
197
+ break;
198
+ case 'length-limit':
199
+ violations = checkLength(text, rule.config);
200
+ break;
201
+ case 'language-filter':
202
+ violations = checkLanguage(text, rule.config);
203
+ break;
204
+ case 'compliance-filter':
205
+ violations = checkCompliance(text);
206
+ break;
207
+ default:
208
+ // Unknown rule — skip
209
+ break;
210
+ }
211
+
212
+ if (violations.length > 0) {
213
+ // Override action from rule config
214
+ violations = violations.map(v => ({ ...v, action: rule.action }));
215
+ allViolations.push(...violations);
216
+
217
+ if (rule.action === 'block') {
218
+ blocked = true;
219
+ blockMessage = `Message blocked by ${rule.name}: ${violations[0].detail}`;
220
+ } else if (rule.action === 'warn') {
221
+ warned = true;
222
+ }
223
+ }
224
+ }
225
+
226
+ return {
227
+ passed: allViolations.length === 0,
228
+ blocked,
229
+ warned,
230
+ redacted,
231
+ message: blocked ? blockMessage : undefined,
232
+ redactedText: redacted ? redactedText : undefined,
233
+ violations: allViolations,
234
+ };
235
+ }
236
+ }
237
+
238
+ // ── Factory from OAD config ─────────────────────────────────
239
+
240
+ export function createGuardrailsFromConfig(config: {
241
+ input?: Array<{ name: string; type: string; action: string; config?: any }>;
242
+ output?: Array<{ name: string; type: string; action: string; config?: any }>;
243
+ }): GuardrailManager {
244
+ return new GuardrailManager({
245
+ input: config.input as GuardrailRule[],
246
+ output: config.output as GuardrailRule[],
247
+ });
248
+ }