Files
dev_agent 72fa311de9
CI / ci (pull_request) Successful in 1m34s
CI / ci (push) Successful in 1m36s
Feature 09: AI harness and skills framework (#62)
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
2026-10-05 11:18:53 -04:00

140 lines
4.5 KiB
Go

package harness
import (
"bytes"
"encoding/json"
"net/url"
"regexp"
"strings"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/llm"
)
// redactionMarker replaces each credential redact removes.
const redactionMarker = "[REDACTED]"
var (
// chatWebhookURL matches a Google Chat webhook URL, with or without
// its scheme.
chatWebhookURL = regexp.MustCompile(`(?i)(?:https?://)?chat\.googleapis\.com/[^\s"'<>]*`)
// schemeURL matches a URL with a scheme, up to whitespace, a quote or
// an angle bracket.
schemeURL = regexp.MustCompile(`(?i)[a-z][a-z0-9+.-]*://[^\s"'<>]+`)
// credentialHeader matches an Authorization or API-key header, or a
// field so named, and its value up to the end of the line or a quote.
// No word boundary is required before the name, so escaped text such
// as a JSON "\nAuthorization: …" matches too.
credentialHeader = regexp.MustCompile(`(?i)(` + credentialHeaderNames + `)(["']?[ \t]*[:=][ \t]*["']?)[^"'\r\n]*`)
)
// credentialHeaderNames are the header names whose values are credentials.
const credentialHeaderNames = `proxy-authorization|authorization|x-goog-api-key|x-api-key|api-key`
// credentialField matches a JSON object key naming a credential header.
var credentialField = regexp.MustCompile(`(?i)^(?:` + credentialHeaderNames + `)$`)
// credentialParams are the substrings that make a URL query or fragment
// parameter name key-like, compared in lower case.
var credentialParams = []string{"key", "token", "secret", "sig", "pass", "auth", "credential"}
// redact returns s with credentials removed: every occurrence of apiKey
// (the decrypted API key, or "" when there is none), Google Chat webhook
// URLs, URLs with a key-like query or fragment parameter or with user
// info, and the values of Authorization and API-key headers. Each becomes
// "[REDACTED]".
//
// When s is a JSON document its strings are redacted after decoding, so
// JSON escapes cannot hide a credential from the patterns, and it is
// encoded again. Any other s is redacted as text.
func redact(s, apiKey string) string {
var doc any
dec := json.NewDecoder(strings.NewReader(s))
dec.UseNumber()
if strings.TrimSpace(s) != "" && json.Valid([]byte(s)) && dec.Decode(&doc) == nil {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
if err := enc.Encode(redactJSON(doc, apiKey)); err != nil {
// A decoded document always encodes; were it not to, it is
// still redacted, as text.
return redactText(s, apiKey)
}
// The encoding can join text into the key; Redact deletes it.
return llm.Redact(strings.TrimSuffix(b.String(), "\n"), apiKey)
}
return redactText(s, apiKey)
}
// redactJSON returns the decoded JSON value v with every string and object
// key redacted, and the values of credential header fields replaced.
func redactJSON(v any, apiKey string) any {
switch v := v.(type) {
case string:
return redact(v, apiKey)
case []any:
for i, e := range v {
v[i] = redactJSON(e, apiKey)
}
return v
case map[string]any:
out := make(map[string]any, len(v))
for k, e := range v {
if credentialField.MatchString(k) {
e = redactionMarker
}
out[redact(k, apiKey)] = redactJSON(e, apiKey)
}
return out
}
return v
}
// redactText redacts s as plain text.
func redactText(s, apiKey string) string {
s = llm.Redact(s, apiKey)
s = chatWebhookURL.ReplaceAllLiteralString(s, redactionMarker)
s = schemeURL.ReplaceAllStringFunc(s, func(u string) string {
if hasCredential(u) {
return redactionMarker
}
return u
})
s = credentialHeader.ReplaceAllString(s, "${1}${2}"+redactionMarker)
// A removal can join text either side into the key; Redact deletes
// it until none remains.
return llm.Redact(s, apiKey)
}
// hasCredential reports whether URL u carries user info or a key-like
// query or fragment parameter. A parameter name that does not unescape
// counts as key-like, so it is never kept unexamined.
func hasCredential(u string) bool {
_, rest, _ := strings.Cut(u, "://")
authority := rest
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
authority = rest[:i]
}
if strings.Contains(authority, "@") {
return true
}
i := strings.IndexAny(rest, "?#")
if i < 0 {
return false
}
params := strings.FieldsFunc(rest[i+1:], func(r rune) bool { return strings.ContainsRune("&;?#", r) })
for _, p := range params {
name, _, _ := strings.Cut(p, "=")
name, err := url.QueryUnescape(name)
if err != nil {
return true
}
name = strings.ToLower(name)
for _, c := range credentialParams {
if strings.Contains(name, c) {
return true
}
}
}
return false
}