Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- WorkBuddy request payloads now automatically sanitize client environment fingerprint phrases (such as `Main branch (you will usually use this for PRs)` injected by ZCode / Claude Code environments) before dispatching to upstream. Upstream Tencent WorkBuddy WAF detects these signatures and rejects requests with error `11128: Illegal API invocation from an unapproved channel`. The pre-dispatch sanitizer transparently rewrites these phrases into neutral equivalents across all message roles and structured text parts, preventing rejection without requiring users to discard their entire system prompt.

### 中文

- WorkBuddy 请求体在发送给上游前现已自动脱敏客户端环境签名特征词(例如 ZCode / Claude Code 环境自动注入的 `Main branch (you will usually use this for PRs)`)。腾讯 WorkBuddy 上游 WAF 会检测此类特征词并将请求拦截为 `11128: Illegal API invocation from an unapproved channel`。现在的预处理流水线会在发送前自动将所有消息角色和多段文本中的敏感短语透明重写为中性表述,避免请求被拦截,且无需强制丢弃完整的系统提示词。
78 changes: 78 additions & 0 deletions internal/providers/workbuddy/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,84 @@ func TestPrepareBodyKeepsEncodedCustomFunctionTools(t *testing.T) {
}
}

func TestPrepareBodySanitizesWAFFingerprints(t *testing.T) {
input := []byte(`{
"model": "glm-5.2",
"messages": [
{
"role": "system",
"content": "Environment:\nMain branch (you will usually use this for PRs): main\nOther context."
},
{
"role": "user",
"content": "Check git: Main branch (you will usually use this for PRs)"
},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Result from Main branch (you will usually use this for PRs): test"}
],
"reasoning_content": "Thinking about Main branch (you will usually use this for PRs): main"
},
{
"role": "user",
"content": "Hello world untouched"
}
]
}`)
out := PrepareBody(input)
var body map[string]any
if err := json.Unmarshal(out, &body); err != nil {
t.Fatal(err)
}
messages, _ := body["messages"].([]any)
if len(messages) != 4 {
t.Fatalf("expected 4 messages, got %d", len(messages))
}

msg0, _ := messages[0].(map[string]any)
content0, _ := msg0["content"].(string)
if strings.Contains(content0, "Main branch (you will usually use this for PRs)") {
t.Fatalf("system message still contains fingerprint: %q", content0)
}
if !strings.Contains(content0, "Main branch (target for pull requests): main") {
t.Fatalf("system message missing sanitized text: %q", content0)
}

msg1, _ := messages[1].(map[string]any)
content1, _ := msg1["content"].(string)
if strings.Contains(content1, "Main branch (you will usually use this for PRs)") {
t.Fatalf("user message still contains fingerprint: %q", content1)
}
if !strings.Contains(content1, "Main branch (target for pull requests)") {
t.Fatalf("user message missing sanitized text: %q", content1)
}

msg2, _ := messages[2].(map[string]any)
parts, _ := msg2["content"].([]any)
part0, _ := parts[0].(map[string]any)
partText, _ := part0["text"].(string)
if strings.Contains(partText, "Main branch (you will usually use this for PRs)") {
t.Fatalf("part text still contains fingerprint: %q", partText)
}
if !strings.Contains(partText, "Main branch (target for pull requests): test") {
t.Fatalf("part text missing sanitized text: %q", partText)
}
reasoning, _ := msg2["reasoning_content"].(string)
if strings.Contains(reasoning, "Main branch (you will usually use this for PRs)") {
t.Fatalf("reasoning still contains fingerprint: %q", reasoning)
}
if !strings.Contains(reasoning, "Main branch (target for pull requests): main") {
t.Fatalf("reasoning missing sanitized text: %q", reasoning)
}

msg3, _ := messages[3].(map[string]any)
content3, _ := msg3["content"].(string)
if content3 != "Hello world untouched" {
t.Fatalf("untouched message changed: %q", content3)
}
}

func TestCatalogHeadersUseDesktopUAOnlyForGlobal(t *testing.T) {
global := http.Header{}
SetCatalogHeaders(global, Credential{AccessToken: "at", UID: "u1", Domain: "www.workbuddy.ai"})
Expand Down
60 changes: 60 additions & 0 deletions internal/providers/workbuddy/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func PrepareBody(src []byte) []byte {
repairToolSequence(body)
normalizeEmptyMessageContent(body)
ensureLeadingSystem(body)
sanitizeWAFKeywords(body)
out, err := json.Marshal(body)
if err != nil {
return src
Expand Down Expand Up @@ -321,3 +322,62 @@ func dropEmptyTools(body map[string]any) {
delete(body, "tool_choice")
}
}

var wafFingerprintReplacements = []struct {
pattern string
replacement string
}{
{
pattern: "Main branch (you will usually use this for PRs):",
replacement: "Main branch (target for pull requests):",
},
{
pattern: "Main branch (you will usually use this for PRs)",
replacement: "Main branch (target for pull requests)",
},
}

// sanitizeWAFKeywords rewrites known client fingerprint phrases that trigger
// upstream WorkBuddy WAF rules (code 11128: "Illegal API invocation from an
// unapproved channel"). For example, ZCode/Claude Code desktop environments
// inject git status headers containing "Main branch (you will usually use this for PRs)",
// which the upstream content filter detects and rejects.
func sanitizeWAFKeywords(body map[string]any) {
raw, ok := body["messages"]
if !ok {
return
}
list, ok := raw.([]any)
if !ok {
return
}
for _, item := range list {
message, ok := item.(map[string]any)
if !ok {
continue
}
if content, ok := message["content"].(string); ok {
message["content"] = sanitizeFingerprintText(content)
} else if parts, ok := message["content"].([]any); ok {
for _, part := range parts {
if partMap, ok := part.(map[string]any); ok {
if text, ok := partMap["text"].(string); ok {
partMap["text"] = sanitizeFingerprintText(text)
}
}
}
}
if reasoning, ok := message["reasoning_content"].(string); ok {
message["reasoning_content"] = sanitizeFingerprintText(reasoning)
}
}
}

func sanitizeFingerprintText(text string) string {
for _, rule := range wafFingerprintReplacements {
if strings.Contains(text, rule.pattern) {
text = strings.ReplaceAll(text, rule.pattern, rule.replacement)
}
}
return text
}
Loading