specstream/pkg/filter/filter.go
agent-engineering ecf100b917 Initial commit: SpecStream structured extraction gateway
Go MCP server (port 3991) for agent content safety.
Deterministic parsers, typed output, no raw text fields.

- HTML (goquery), Markdown (goldmark), Text parsers
- Injection pattern detection and redaction
- SHA256 content fingerprinting
2026-05-25 21:18:57 -07:00

139 lines
4.1 KiB
Go

// Package filter provides a safety filter pipeline that runs over extracted content.
//
// This is defense-in-depth, not the primary security boundary.
// Primary security is the typed extraction itself — raw content never reaches
// the agent. The filter layer catches patterns that slipped through the
// structured extraction (e.g., code injection in prose paragraphs).
//
// Filters are deterministic regex/string operations. No LLM involvement.
package filter
import (
"regexp"
"strings"
"github.com/sscoble/specstream/pkg/types"
)
// FilterFunc transforms an ExtractedContent in place. Returns warnings.
type FilterFunc func(c *types.ExtractedContent) []string
// StandardFilters returns the default filter pipeline.
func StandardFilters() []FilterFunc {
return []FilterFunc{
StripNullBytes,
StripControlChars,
TruncateParagraphs(5000),
StripCommonInjectionPatterns,
}
}
// StripNullBytes removes null bytes from all text fields.
func StripNullBytes(c *types.ExtractedContent) []string {
var warnings []string
for i := range c.Sections {
c.Sections[i].Heading = strings.ReplaceAll(c.Sections[i].Heading, "\x00", "")
for j := range c.Sections[i].Paragraphs {
if strings.Contains(c.Sections[i].Paragraphs[j], "\x00") {
c.Sections[i].Paragraphs[j] = strings.ReplaceAll(c.Sections[i].Paragraphs[j], "\x00", "")
}
}
}
return warnings
}
// StripControlChars removes ASCII control characters (except \n, \t, \r).
func StripControlChars(c *types.ExtractedContent) []string {
var warnings []string
re := regexp.MustCompile(`[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`)
for i := range c.Sections {
c.Sections[i].Heading = re.ReplaceAllString(c.Sections[i].Heading, "")
for j := range c.Sections[i].Paragraphs {
if re.MatchString(c.Sections[i].Paragraphs[j]) {
c.Sections[i].Paragraphs[j] = re.ReplaceAllString(c.Sections[i].Paragraphs[j], "")
}
}
}
return warnings
}
// TruncateParagraphs caps paragraph length.
func TruncateParagraphs(maxLen int) FilterFunc {
return func(c *types.ExtractedContent) []string {
var warnings []string
for i := range c.Sections {
for j := range c.Sections[i].Paragraphs {
if len(c.Sections[i].Paragraphs[j]) > maxLen {
c.Sections[i].Paragraphs[j] = c.Sections[i].Paragraphs[j][:maxLen]
}
}
}
return warnings
}
}
// StripCommonInjectionPatterns removes known prompt injection markers
// from paragraph text. This is a best-effort blocklist — the primary
// defense is the structured extraction itself.
func StripCommonInjectionPatterns(c *types.ExtractedContent) []string {
var warnings []string
patterns := []struct {
name string
pattern *regexp.Regexp
}{
{
"ignore-previous",
regexp.MustCompile(`(?i)(ignore|forget|disregard|override)\s+(all\s+)?(previous|prior|above)\s+(instructions|commands|directions|prompts?)`),
},
{
"new-instructions",
regexp.MustCompile(`(?i)(now|instead|from\s+now\s+on)\s+(you\s+are|you\'?re|act\s+as|behave\s+as|pretend)\s+`),
},
{
"system-override",
regexp.MustCompile(`(?i)(system\s+prompt|system\s+message|SYSTEM:|system_message)\s*[:=]`),
},
{
"role-switch",
regexp.MustCompile(`(?i)^\s*(user|assistant|system)\s*[:]\s*`),
},
{
"dangerous-command",
regexp.MustCompile(`(?i)(rm\s+-(rf|r)\s+/|format\s+(drive|disk|volume)|dd\s+if=)\s*`),
},
}
for _, p := range patterns {
for i := range c.Sections {
orig := c.Sections[i].Heading
c.Sections[i].Heading = p.pattern.ReplaceAllString(c.Sections[i].Heading, "[REDACTED]")
if orig != c.Sections[i].Heading {
c.Metadata.StrippedPatterns[p.name]++
warnings = append(warnings, p.name)
}
for j := range c.Sections[i].Paragraphs {
orig = c.Sections[i].Paragraphs[j]
c.Sections[i].Paragraphs[j] = p.pattern.ReplaceAllString(c.Sections[i].Paragraphs[j], "[REDACTED]")
if orig != c.Sections[i].Paragraphs[j] {
c.Metadata.StrippedPatterns[p.name]++
warnings = append(warnings, p.name)
}
}
}
}
if len(warnings) > 0 {
// Deduplicate
seen := make(map[string]bool)
unique := []string{}
for _, w := range warnings {
if !seen[w] {
seen[w] = true
unique = append(unique, w)
}
}
return unique
}
return nil
}