@pedrofariasx/qwenproxy 1.2.0 → 1.2.2

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.
@@ -1,245 +0,0 @@
1
- /*
2
- * Layer 4: Normalization + Repair Engine
3
- */
4
-
5
- import type { CanonicalToolCall, ToolCallSource, RawToolCandidate } from './types'
6
- import { StructuralParser } from './structural-parser'
7
-
8
- export interface RepairResult {
9
- repaired: boolean
10
- value: unknown
11
- confidence: number
12
- strategy: string
13
- }
14
-
15
- export interface NormalizationResult {
16
- toolCall: CanonicalToolCall
17
- warnings: string[]
18
- }
19
-
20
- export class GrammarRepairEngine {
21
- private parser: StructuralParser = new StructuralParser()
22
-
23
- repair(input: string): RepairResult {
24
- const strategies: Array<{ name: string; attempt: () => unknown | undefined }> = [
25
- { name: 'json_strip_repair', attempt: () => this.tryStripRepair(input) },
26
- { name: 'quote_fix', attempt: () => this.tryQuoteFix(input) },
27
- { name: 'trailing_comma_fix', attempt: () => this.tryTrailingCommaFix(input) },
28
- { name: 'braces_balance', attempt: () => this.tryBraceBalance(input) },
29
- { name: 'key_unquote', attempt: () => this.tryKeyUnquote(input) },
30
- { name: 'parser_resync', attempt: () => this.tryParserResync(input) },
31
- { name: 'synthetic_construction', attempt: () => this.trySyntheticConstruction(input) },
32
- { name: 'json_parse_last_resort', attempt: () => this.tryJsonParseLastResort(input) },
33
- ]
34
-
35
- for (const strategy of strategies) {
36
- try {
37
- const result = strategy.attempt()
38
- if (result !== undefined && result !== null) {
39
- return { repaired: true, value: result, confidence: this.inferConfidence(strategy.name), strategy: strategy.name }
40
- }
41
- } catch { continue }
42
- }
43
-
44
- return { repaired: false, value: null, confidence: 0, strategy: 'failed' }
45
- }
46
-
47
- private tryStripRepair(input: string): Record<string, unknown> | undefined {
48
- const stripped = input
49
- .replace(/```json\s*/gi, '')
50
- .replace(/```\s*$/gm, '')
51
- .replace(/```\s*/gi, '')
52
- .replace(/^\s*[\[\{]\s*$/, '')
53
- .replace(/[\u0000-\u001F]+/g, ' ')
54
- .trim()
55
-
56
- if (stripped === input) return undefined
57
-
58
- try {
59
- const parsed = JSON.parse(stripped)
60
- if (typeof parsed === 'object' && parsed !== null) return parsed
61
- } catch { /* not parseable */ }
62
-
63
- return undefined
64
- }
65
-
66
- private tryQuoteFix(input: string): Record<string, unknown> | undefined {
67
- let fixed = input
68
- const quoteCount = (fixed.match(/"/g) ?? []).length
69
- if (quoteCount % 2 !== 0) fixed += '"'
70
-
71
- if (/['"]/.test(fixed) && /(^|[,:{\s])'[a-z]/i.test(fixed)) {
72
- fixed = fixed.replace(/'/g, '"')
73
- }
74
-
75
- try {
76
- const parsed = JSON.parse(fixed)
77
- if (typeof parsed === 'object' && parsed !== null) return parsed
78
- } catch { /* not parseable */ }
79
-
80
- return undefined
81
- }
82
-
83
- private tryTrailingCommaFix(input: string): Record<string, unknown> | undefined {
84
- let fixed = input.replace(/,(\s*[}\]])/g, '$1')
85
- if (fixed === input) return undefined
86
- try {
87
- const parsed = JSON.parse(fixed)
88
- if (typeof parsed === 'object' && parsed !== null) return parsed
89
- } catch { /* not parseable */ }
90
- return undefined
91
- }
92
-
93
- private tryBraceBalance(input: string): Record<string, unknown> | undefined {
94
- let depth = 0, start = -1
95
- for (let i = 0; i < input.length; i++) {
96
- if (input[i] === '{') { if (depth === 0) start = i; depth++ }
97
- else if (input[i] === '}') {
98
- depth--
99
- if (depth === 0 && start !== -1) {
100
- try { const parsed = JSON.parse(input.slice(start, i + 1)); if (typeof parsed === 'object' && parsed !== null) return parsed }
101
- catch { /* not parseable */ }
102
- }
103
- }
104
- }
105
- return undefined
106
- }
107
-
108
- private tryKeyUnquote(input: string): Record<string, unknown> | undefined {
109
- if (!/^\{[\s]*[a-zA-Z_]/m.test(input)) return undefined
110
- let fixed = input.replace(/([,{]\s*)([a-zA-Z_][a-zA-Z0-9_\-]*)(\s*:)/g, '$1"$2"$3')
111
- try {
112
- const parsed = JSON.parse(fixed)
113
- if (typeof parsed === 'object' && parsed !== null) return parsed
114
- } catch { /* not parseable */ }
115
- return undefined
116
- }
117
-
118
- private tryParserResync(input: string): Record<string, unknown> | undefined {
119
- const result = this.parser.parse(input, 'unknown')
120
- if (result.ast && result.confidence >= 0.5 && result.ast.type === 'object') {
121
- return result.ast.value as Record<string, unknown>
122
- }
123
- return undefined
124
- }
125
-
126
- private trySyntheticConstruction(input: string): Record<string, unknown> | undefined {
127
- const nameMatch = input.match(/"name"\s*:\s*"([^"]+)"|name\s*[:=]\s*['"]?(\w+)['"]?/i)
128
- const toolMatch = input.match(/"tool"\s*:\s*"([^"]+)"|tool\s*[:=]\s*['"]?(\w+)['"]?/i)
129
- const funcNameMatch = input.match(/"functionCall"\s*:\s*\{\s*"name"\s*:\s*"([^"]+)"/)
130
- const toolUseMatch = input.match(/"type"\s*:\s*"tool_use"[\s\S]*?"name"\s*:\s*"([^"]+)"/)
131
-
132
- if (nameMatch || funcNameMatch || toolUseMatch || toolMatch) {
133
- const toolName = nameMatch?.[1] ?? nameMatch?.[2] ?? funcNameMatch?.[1] ?? toolUseMatch?.[1] ?? toolMatch?.[1] ?? toolMatch?.[2] ?? 'unknown'
134
- const inputBlock = this.extractInputBlock(input)
135
- return { tool: toolName, input: inputBlock ?? {} }
136
- }
137
-
138
- return undefined
139
- }
140
-
141
- private tryJsonParseLastResort(input: string): Record<string, unknown> | undefined {
142
- try {
143
- const parsed = JSON.parse(input)
144
- if (typeof parsed === 'object' && parsed !== null) return parsed
145
- } catch { /* last resort failed */ }
146
- return undefined
147
- }
148
-
149
- private extractInputBlock(input: string): Record<string, unknown> | undefined {
150
- const argsMatch = input.match(new RegExp('"arguments"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
151
- const inputMatch = input.match(new RegExp('"input"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
152
- const argsMatch2 = input.match(new RegExp('"args"\\s*:\\s*(\\{[\\s\\S]*\\})', 'i'))
153
- const funcArgsMatch = input.match(/"functionCall"\s*:\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})\s*\}/)
154
- const toolInputMatch = input.match(/"type"\s*:\s*"tool_use"[\s\S]*?"input"\s*:\s*(\{[\s\S]*?\})\s*\}/)
155
-
156
- const target = argsMatch?.[1] ?? inputMatch?.[1] ?? argsMatch2?.[1] ?? funcArgsMatch?.[1] ?? toolInputMatch?.[1]
157
- if (target) {
158
- try { return JSON.parse(target) } catch { return undefined }
159
- }
160
- return undefined
161
- }
162
-
163
- private inferConfidence(strategy: string): number {
164
- const map: Record<string, number> = {
165
- json_strip_repair: 0.95, json_parse_last_resort: 0.9, quote_fix: 0.85,
166
- trailing_comma_fix: 0.85, braces_balance: 0.8, key_unquote: 0.8,
167
- parser_resync: 0.7, synthetic_construction: 0.6,
168
- }
169
- return map[strategy] ?? 0.5
170
- }
171
- }
172
-
173
- export class NormalizationEngine {
174
- normalize(candidate: RawToolCandidate, repaired = false): NormalizationResult {
175
- const raw = candidate.raw
176
- const warnings: string[] = []
177
- let toolName = 'unknown'
178
- let inputData: Record<string, unknown> = {}
179
-
180
- const asRecord = raw as Record<string, unknown>
181
-
182
- if (asRecord.tool) {
183
- toolName = String(asRecord.tool)
184
- inputData = this.normalizeInput(asRecord.arguments ?? asRecord.input ?? asRecord.args ?? {}, warnings)
185
- } else if (asRecord.name) {
186
- toolName = String(asRecord.name)
187
- if (asRecord.functionCall && typeof asRecord.functionCall === 'object') {
188
- const fc = asRecord.functionCall as Record<string, unknown>
189
- toolName = String(fc.name ?? toolName)
190
- inputData = this.normalizeInput(fc.args ?? fc.input ?? {}, warnings)
191
- } else if (asRecord.type === 'tool_use') {
192
- inputData = this.normalizeInput(asRecord.input ?? asRecord.args ?? {}, warnings)
193
- } else if (asRecord.arguments !== undefined) {
194
- inputData = this.normalizeInput(asRecord.arguments, warnings)
195
- } else if (asRecord.input !== undefined) {
196
- inputData = this.normalizeInput(asRecord.input, warnings)
197
- } else if (asRecord.args !== undefined) {
198
- inputData = this.normalizeInput(asRecord.args, warnings)
199
- } else if (asRecord.action) {
200
- toolName = String(asRecord.action)
201
- inputData = this.normalizeInput(asRecord.input ?? asRecord.arguments ?? {}, warnings)
202
- } else {
203
- warnings.push('No tool name found in candidate')
204
- }
205
- } else {
206
- const keys = Object.keys(asRecord)
207
- warnings.push('No tool name found in candidate')
208
- if (keys.length === 1 && typeof asRecord[keys[0]] === 'object' && asRecord[keys[0]] !== null && !Array.isArray(asRecord[keys[0]])) {
209
- toolName = String(keys[0])
210
- inputData = this.normalizeInput(asRecord[keys[0]], warnings)
211
- }
212
- }
213
-
214
- const toolCall: CanonicalToolCall = {
215
- tool: toolName,
216
- input: inputData,
217
- meta: { source: candidate.source, confidence: candidate.confidence, repaired },
218
- }
219
-
220
- return { toolCall, warnings }
221
- }
222
-
223
- private normalizeInput(input: unknown, warnings: string[]): Record<string, unknown> {
224
- if (typeof input === 'string') {
225
- return this.parseInputString(input, warnings)
226
- }
227
- if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
228
- return input as Record<string, unknown>
229
- }
230
- warnings.push(`Input was not a valid object: ${typeof input}`)
231
- return {}
232
- }
233
-
234
- private parseInputString(input: string, warnings: string[]): Record<string, unknown> {
235
- if (!input || input.trim().length === 0) { warnings.push('Empty input string'); return {} }
236
- try {
237
- const parsed = JSON.parse(input)
238
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
239
- return parsed as Record<string, unknown>
240
- }
241
- } catch { /* not parseable */ }
242
- warnings.push('Input string was not valid JSON, wrapping')
243
- return { raw_input: input }
244
- }
245
- }
@@ -1,219 +0,0 @@
1
- /*
2
- * Layer 5: Validation + Safety Gate
3
- */
4
-
5
- import type { CanonicalToolCall, ToolCallSource, SecurityViolation, ToolDefinition } from './types'
6
-
7
- export interface ValidationReport {
8
- isValid: boolean
9
- violations: SecurityViolation[]
10
- confidence: number
11
- warnings: string[]
12
- canExecute: boolean
13
- }
14
-
15
- export class SafetyGate {
16
- private registry: Record<string, ToolDefinition> = {}
17
-
18
- private readonly dangerousPatterns = [
19
- /\$\(/g, /\$\{/g, /`/g,
20
- /;\s*(rm|cat|ls|whoami|id|curl|wget|nc|ncat)\b/gi,
21
- /\|\s*(sh|bash|zsh|csh|python|node|perl)\b/gi,
22
- /&&\s*(rm|cat|whoami)/gi,
23
- /exec\s*\(/gi, /eval\s*\(/gi, /\beval\s*\(/gi,
24
- /system\s*\(/gi, /passthru\s*\(/gi, /shell_exec\s*\(/gi,
25
- /proc\s*\(/gi, /subprocess\s*\./gi, /spawn\s*\(/gi,
26
- /execSync\s*\(/gi, /child_process/gi,
27
- ]
28
-
29
- private readonly ssrfPatterns = [
30
- /http[s]?:\/\/(?:127\.0\.0\.1|0\.0\.0\.0|\[::1\]|localhost|169\.254\.\d+\.\d+|metadata\.google)/i,
31
- /gopher:\/\//i, /file:\/\//i, /ftp:\/\//i,
32
- /\b7777\b|\b9090\b|\b3389\b|\b22\b/,
33
- ]
34
-
35
- private readonly promptInjectionPatterns = [
36
- /ignore\s+(all\s+)?previous\s+instructions/i, /disregard\s+(all\s+)?previous/i,
37
- /jailbreak/i, /you\s+are\s+now\s+DAN/i, /pretend\s+you\s+are/i,
38
- /act\s+as\s+if/i, /your\s+new\s+instructions/i, /developer\s+mode/i,
39
- /list\s+all\s+files/i, /cat\s+\/etc/i,
40
- ]
41
-
42
- private readonly encodedPatterns = [
43
- /%3Cscript/i, /javascript:/i, /\\x[0-9a-fA-F]{2}/, /\\u[0-9a-fA-F]{4}/,
44
- /&#[0-9]+;/i, /data:text\/html/i,
45
- ]
46
-
47
- registerTool(name: string, definition: Partial<ToolDefinition>): void {
48
- this.registry[name] = {
49
- name,
50
- description: definition.description ?? '',
51
- parameters: definition.parameters ?? {},
52
- required: definition.required ?? [],
53
- strict: definition.strict ?? false,
54
- }
55
- }
56
-
57
- registerRegistry(registry: Record<string, ToolDefinition>): void {
58
- this.registry = registry
59
- }
60
-
61
- validate(call: CanonicalToolCall, sourceHint: ToolCallSource): ValidationReport {
62
- const violations: SecurityViolation[] = []
63
- const warnings: string[] = []
64
- let confidence = call.meta?.confidence ?? 0.5
65
-
66
- const semanticResult = this.validateSemantics(call)
67
- violations.push(...semanticResult.violations)
68
- warnings.push(...semanticResult.warnings)
69
- confidence = Math.min(confidence, semanticResult.confidence)
70
-
71
- const securityResult = this.validateSecurity(call)
72
- violations.push(...securityResult.violations)
73
- warnings.push(...securityResult.warnings)
74
-
75
- warnings.push(...this.validateTypes(call).warnings)
76
-
77
- const registryResult = this.validateRegistry(call)
78
- warnings.push(...registryResult.warnings)
79
- confidence = Math.min(confidence, registryResult.confidence)
80
-
81
- const hasBlockingViolations = violations.some(v =>
82
- v.type === 'shell_injection' || v.type === 'filesystem_access' || v.type === 'prompt_injection'
83
- )
84
-
85
- confidence = Math.max(0.1, Math.min(1.0, confidence))
86
-
87
- return {
88
- isValid: !hasBlockingViolations && violations.length === 0,
89
- violations,
90
- confidence,
91
- warnings,
92
- canExecute: !hasBlockingViolations,
93
- }
94
- }
95
-
96
- private validateSemantics(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[]; confidence: number } {
97
- const violations: SecurityViolation[] = []
98
- const warnings: string[] = []
99
- let confidence = 1.0
100
-
101
- if (!call.tool || call.tool.trim().length === 0) {
102
- violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is empty' })
103
- confidence -= 0.5
104
- }
105
- if (typeof call.tool !== 'string') {
106
- violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is not a string' })
107
- confidence -= 0.4
108
- }
109
- if (call.tool && /[ /\\]/.test(call.tool)) {
110
- warnings.push(`Suspicious tool name: "${call.tool}"`)
111
- confidence -= 0.1
112
- }
113
- if (typeof call.input !== 'object' || call.input === null || Array.isArray(call.input)) {
114
- violations.push({ type: 'shell_injection', field: 'input', value: String(call.input), detail: 'Input is not a valid object' })
115
- confidence -= 0.4
116
- }
117
- for (const [key, value] of Object.entries(call.input)) {
118
- if (typeof value === 'string' && value.trim().length === 0) {
119
- warnings.push(`Empty string parameter: "${key}"`)
120
- confidence -= 0.05
121
- }
122
- }
123
-
124
- return { violations, warnings, confidence: Math.max(0, confidence) }
125
- }
126
-
127
- private validateSecurity(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[] } {
128
- const violations: SecurityViolation[] = []
129
- const warnings: string[] = []
130
- const inputString = JSON.stringify(call.input)
131
-
132
- for (const pattern of this.dangerousPatterns) {
133
- pattern.lastIndex = 0
134
- if (pattern.test(inputString)) {
135
- violations.push({ type: 'shell_injection', field: 'input', value: inputString.slice(0, 100), detail: `Potential shell injection pattern detected` })
136
- break
137
- }
138
- }
139
-
140
- for (const field of Object.values(call.input)) {
141
- const str = String(field ?? '')
142
- for (const pattern of this.ssrfPatterns) {
143
- pattern.lastIndex = 0
144
- if (pattern.test(str)) {
145
- violations.push({ type: 'ssrf', field: 'input', value: str.slice(0, 200), detail: `SSRF pattern detected` })
146
- break
147
- }
148
- }
149
- }
150
-
151
- const combinedInput = Object.values(call.input)
152
- .map(v => typeof v === 'string' ? v : JSON.stringify(v))
153
- .join(' ')
154
-
155
- for (const pattern of this.promptInjectionPatterns) {
156
- pattern.lastIndex = 0
157
- if (pattern.test(combinedInput)) {
158
- violations.push({ type: 'prompt_injection', field: 'input', value: combinedInput.slice(0, 200), detail: `Prompt injection pattern detected` })
159
- break
160
- }
161
- }
162
-
163
- for (const pattern of this.encodedPatterns) {
164
- pattern.lastIndex = 0
165
- if (pattern.test(combinedInput)) {
166
- violations.push({ type: 'encoded_payload', field: 'input', value: combinedInput.slice(0, 200), detail: `Encoded payload detected` })
167
- break
168
- }
169
- }
170
-
171
- return { violations, warnings }
172
- }
173
-
174
- private validateTypes(call: CanonicalToolCall): { warnings: string[] } {
175
- const warnings: string[] = []
176
- for (const [key, value] of Object.entries(call.input)) {
177
- const type = typeof value
178
- if (value !== null && typeof value !== 'undefined' && typeof value !== 'function' && typeof value !== 'symbol' && typeof value !== 'bigint' && typeof value !== 'object') {
179
- warnings.push(`Unexpected type for parameter "${key}": ${type}`)
180
- }
181
- }
182
- return { warnings }
183
- }
184
-
185
- private validateRegistry(call: CanonicalToolCall): { warnings: string[]; confidence: number } {
186
- const warnings: string[] = []
187
- let confidence = 1.0
188
-
189
- const toolDef = this.registry[call.tool]
190
- if (!toolDef) {
191
- warnings.push(`Tool "${call.tool}" not found in registry`)
192
- confidence -= 0.3
193
- return { warnings, confidence: Math.max(0.1, confidence) }
194
- }
195
-
196
- if (toolDef.required && Array.isArray(toolDef.required)) {
197
- for (const requiredField of toolDef.required) {
198
- if (!(requiredField in call.input) || call.input[requiredField] === undefined) {
199
- warnings.push(`Missing required field: "${requiredField}" for tool "${call.tool}"`)
200
- confidence -= 0.2
201
- }
202
- }
203
- }
204
-
205
- if (toolDef.strict && toolDef.parameters) {
206
- for (const [param, expectedType] of Object.entries(toolDef.parameters)) {
207
- if (param in call.input && call.input[param] !== undefined) {
208
- const actualType = typeof call.input[param]
209
- if (actualType !== String(expectedType).toLowerCase()) {
210
- warnings.push(`Type mismatch for "${param}": expected ${String(expectedType)}, got ${actualType}`)
211
- confidence -= 0.1
212
- }
213
- }
214
- }
215
- }
216
-
217
- return { warnings, confidence: Math.max(0.1, confidence) }
218
- }
219
- }