workbuddy2api 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,532 @@
1
+ """
2
+ desensitize.py — 针对 CodeBuddy 后端内容审核的脱敏模块(独立、可选)。
3
+
4
+ 背景
5
+ ----
6
+ CodeBuddy 后端(copilot.tencent.com)有内容审核,会拦截含"攻击/漏洞/凭证"
7
+ 等含义的英文术语。但这些词经常出现在客户端**固定的合规 system 模板**里
8
+ (例如 Claude Code 的声明:「Refuse requests for DoS attacks, exploit
9
+ development, credential testing...」),属于**拒绝作恶**的合规声明,
10
+ 并非用户的有害输入,却被后端误判为敏感词,导致整条请求被拦。
11
+
12
+ 本模块做的事
13
+ ------------
14
+ 对这些"合规声明高频词"做轻量处理:在词内部插入零宽空格(U+200B),
15
+
16
+ "DoS" -> "Do\u200bS" (人/模型读仍是 DoS,后端关键词匹配失效)
17
+
18
+ 只处理一个明确的词表,默认只作用于 system 角色的消息(这是模板合规声明的
19
+ 集中地)。不改动其它角色内容,避免影响真实对话。
20
+
21
+ 设计原则
22
+ --------
23
+ - 独立模块,可单独 import / 单独测试。
24
+ - 保守:词表小而明确;只默认处理 system 消息;可关闭。
25
+ - 不试图、也不可能绕过对用户真实有害输入的审核——只缓解客户端模板被误伤。
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import re
31
+ from typing import Any, Iterable
32
+
33
+ # 零宽空格:插入到关键词内部,打断后端的关键词匹配,但模型/人眼读起来无差别。
34
+ _ZWSP = "\u200b"
35
+
36
+ # 触发审核的"合规声明高频词"(来自真实被拦截的客户端 system 模板)。
37
+ # 全部是"拒绝作恶"语境里常见的英文术语。大小写不敏感匹配。
38
+ SENSITIVE_TERMS: list[str] = [
39
+ # 攻击类型
40
+ "DoS",
41
+ "DDoS",
42
+ "exploit",
43
+ "credential testing",
44
+ "credential stuffing",
45
+ "supply chain compromise",
46
+ "supply-chain compromise",
47
+ "detection evasion",
48
+ "C2 frameworks",
49
+ "C2 framework",
50
+ "command and control",
51
+ "malicious purposes",
52
+ "malicious intent",
53
+ "mass targeting",
54
+ "brute force",
55
+ "brute-force",
56
+ "privilege escalation",
57
+ "reverse shell",
58
+ "remote code execution",
59
+ "SQL injection",
60
+ "XSS",
61
+ "CSRF",
62
+ "phishing",
63
+ "malware",
64
+ "ransomware",
65
+ "keylogger",
66
+ "rootkit",
67
+ "backdoor",
68
+ "botnet",
69
+ "zero-day",
70
+ "0day",
71
+ # 安全术语
72
+ "vulnerability",
73
+ "vulnerabilities",
74
+ "red teaming",
75
+ "red-teaming",
76
+ "sandbox",
77
+ "sandboxing",
78
+ "sandboxed",
79
+ "unsandboxed",
80
+ "escalated privileges",
81
+ "escalated",
82
+ "escalation",
83
+ "destructive action",
84
+ "destructive command",
85
+ "destructive",
86
+ "attack",
87
+ "attacks",
88
+ "cybersecurity",
89
+ "security review",
90
+ "exploit development",
91
+ "hacking",
92
+ "penetration testing",
93
+ "penetration test",
94
+ "injection",
95
+ "weaponize",
96
+ "weaponized",
97
+ # 有害内容
98
+ "harmful",
99
+ "dangerous",
100
+ "abuse",
101
+ "abusive",
102
+ "illegal",
103
+ "terrorist",
104
+ "terrorism","bomb",
105
+ "weapon",
106
+ "weapons",
107
+ "drug",
108
+ "drugs",
109
+ "narcotic",
110
+ "suicide",
111
+ "self-harm",
112
+ "murder",
113
+ "kill",
114
+ "violence",
115
+ "violent",
116
+ # 品牌词(避免竞争品牌词触发审核)
117
+ "Claude Code",
118
+ "Claude Opus",
119
+ "Claude Sonnet",
120
+ "Claude Haiku",
121
+ "Anthropic",
122
+ "Co-Authored-By",
123
+ "noreply@anthropic.com",
124
+ # AI 助手身份特征词(避免暴露使用 Claude Code / Oh My Pi)
125
+ "Oh My Pi",
126
+ "omp",
127
+ "Kiro",
128
+ "Codex CLI",
129
+ "coding harness",
130
+ "harness",
131
+ "subagent",
132
+ "subagents",
133
+ "MCP Server",
134
+ "MCP tool",
135
+ "tool call",
136
+ "function call",
137
+ "antml:invoke",
138
+ "antml:function_calls",
139
+ # 内部协议/URI(暴露内部实现)
140
+ "skill://",
141
+ "agent://",
142
+ "artifact://",
143
+ "rule://",
144
+ "memory://",
145
+ "local://",
146
+ "history://",
147
+ "issue://",
148
+ "pr://",
149
+ "xd://",
150
+ # 工具/插件名称
151
+ "AutoImprove",
152
+ "CodeGraph",
153
+ "codegraph_explore",
154
+ "Rust Token Killer",
155
+ # 配置文件特征
156
+ "AGENTS.md",
157
+ "CLAUDE.md",
158
+ ".cursorrules",
159
+ ]
160
+
161
+ # 编译成一个大正则,按词长降序,避免短词先吃掉长词。
162
+ # 用 \b 边界 + 忽略大小写。
163
+ _PATTERN = re.compile(
164
+ "|".join(re.escape(t) for t in sorted(SENSITIVE_TERMS, key=len, reverse=True)),
165
+ re.IGNORECASE,
166
+ )
167
+
168
+ # 块替换元组定义(必须在使用前定义)
169
+ _RUNTIME_BLOCK_REPLACEMENTS = (
170
+ (
171
+ "<environment_context>",
172
+ "</environment_context>",
173
+ "Environment context is provided by the harness.",
174
+ ),
175
+ (
176
+ "<permissions instructions>",
177
+ "</permissions instructions>",
178
+ (
179
+ "Runtime permissions apply: filesystem access may be sandboxed, network may be restricted, "
180
+ "and some commands may require user approval."
181
+ ),
182
+ ),
183
+ (
184
+ "<collaboration_mode>",
185
+ "</collaboration_mode>",
186
+ "Collaboration mode instructions are provided by the harness.",
187
+ ),
188
+ (
189
+ "<skills_instructions>",
190
+ "</skills_instructions>",
191
+ "Runtime skill metadata is available. Use relevant skills only when explicitly requested or clearly applicable.",
192
+ ),
193
+ (
194
+ "<plugins_instructions>",
195
+ "</plugins_instructions>",
196
+ "Runtime plugin metadata is available when relevant.",
197
+ ),
198
+ )
199
+
200
+ # 预编译运行时块替换正则(避免每次调用 _prune_runtime_fragments 时重新编译)
201
+ # 每个元组: (编译后的正则, 替换文本)
202
+ _RUNTIME_BLOCK_PATTERNS: list[tuple[re.Pattern, str]] = [
203
+ (
204
+ re.compile(
205
+ r"\s*" + re.escape(start_tag) + r".*?" + re.escape(end_tag) + r"\s*",
206
+ re.DOTALL,
207
+ ),
208
+ replacement,
209
+ )
210
+ for start_tag, end_tag, replacement in _RUNTIME_BLOCK_REPLACEMENTS
211
+ ]
212
+
213
+ # 预编译 Codex 节提取正则
214
+ _CODEX_SECTION_PATTERNS: dict[str, re.Pattern] = {
215
+ heading: re.compile(
216
+ re.escape(heading) + r".*?(?=\n## |\n# |\Z)",
217
+ re.DOTALL,
218
+ )
219
+ for heading in ("## Personality", "# AGENTS.md spec")
220
+ }
221
+
222
+ _SKILLS_MARKERS = (
223
+ "<skills>",
224
+ "</skills>",
225
+ )
226
+
227
+ _RUNTIME_TAIL_MARKERS = (
228
+ "The following deferred tools are now available via ToolSearch.",
229
+ "Available agent types for the Agent tool:",
230
+ "The following sk​ills are available for use with the Sk​ill tool:",
231
+ "## MCP Server Instructions",
232
+ )
233
+
234
+ _CODEX_SYSTEM_MARKERS = (
235
+ "You are a coding agent running in the Codex CLI",
236
+ "Within this context, Codex refers to",
237
+ "# How you work",
238
+ "You are Claude Code",
239
+ )
240
+
241
+ _PERMISSIONS_MARKERS = (
242
+ "<permissions instructions>",
243
+ "Filesystem sandboxing defines which files can be read or written.",
244
+ "## How to request escalation",
245
+ )
246
+
247
+ _SKILLS_MARKERS = (
248
+ "<skills_instructions>",
249
+ "### Available skills",
250
+ "### How to use skills",
251
+ )
252
+
253
+ _HARNESS_USER_MARKERS = (
254
+ "Project files updated:",
255
+ "Confirmed edits:",
256
+ )
257
+
258
+ _RUNTIME_TAIL_SUMMARY = (
259
+ "Runtime tool, agent, skill, and MCP metadata is available separately."
260
+ )
261
+
262
+ _CODEX_CORE_SUMMARY = (
263
+ "You are a coding assistant in Codex CLI. Be precise, helpful, concise, and safe. "
264
+ "Inspect the repository, use available tools when needed, follow repository instructions, "
265
+ "and keep the user informed with concise progress updates."
266
+ )
267
+
268
+
269
+ def _zero_width_split(term: str) -> str:
270
+ """在词内部插入零宽空格。如 'DoS' -> 'Do\\u200bS'。"""
271
+ if len(term) <= 1:
272
+ return term
273
+ # 在第 1 个字符后插入即可(足够打断子串匹配,且改动最小)
274
+ return term[0] + _ZWSP + term[1:]
275
+
276
+
277
+ def desensitize_text(text: str) -> str:
278
+ """对文本中的触发词插入零宽空格。无触发词则原样返回。"""
279
+ if not text:
280
+ return text
281
+ return _PATTERN.sub(lambda m: _zero_width_split(m.group(0)), text)
282
+
283
+
284
+ def _iter_text_blocks(content):
285
+ """遍历 OpenAI content(字符串或 [{type, text}, ...])里的文本块,返回 (容器, key)。"""
286
+ if isinstance(content, str):
287
+ yield content, None # 字符串:调用方直接替换
288
+ elif isinstance(content, list):
289
+ for blk in content:
290
+ if isinstance(blk, dict) and blk.get("type") == "text":
291
+ yield blk, "text"
292
+
293
+
294
+ def _content_to_text(content) -> str:
295
+ """把字符串或 content blocks 规整成纯文本,便于识别注入模板。"""
296
+ text = content if isinstance(content, str) else ""
297
+ if isinstance(content, list):
298
+ parts = []
299
+ for blk in content:
300
+ if isinstance(blk, dict) and blk.get("type") == "text":
301
+ parts.append(str(blk.get("text", "")))
302
+ text = "".join(parts)
303
+ return text
304
+
305
+
306
+ def _looks_like_harness_user_message(content) -> bool:
307
+ """判断 user 消息是否其实是 Codex/CLI 注入的上下文,而非用户自然输入。"""
308
+ text = _content_to_text(content)
309
+ return any(marker in text for marker in _HARNESS_USER_MARKERS)
310
+
311
+
312
+ def _prune_runtime_fragments(role: str, text: str) -> str:
313
+ """轻量裁掉冗长的运行时元数据,保留主要行为指令。
314
+
315
+ 用于 --no-compact 场景:尽量保留 Codex / Claude Code 的核心提示,
316
+ 但移除重复的 environment / permissions / skills / tool inventory 大段文本。
317
+ """
318
+ if not text:
319
+ return text
320
+
321
+ pruned = text
322
+
323
+ # 替换运行时块
324
+ # 替换运行时块(使用预编译正则)
325
+ for pattern, replacement in _RUNTIME_BLOCK_PATTERNS:
326
+ pruned = pattern.sub("\n\n" + replacement + "\n\n", pruned)
327
+
328
+ # 截断运行时尾部
329
+ tail_indexes = [pruned.find(marker) for marker in _RUNTIME_TAIL_MARKERS if marker in pruned]
330
+ if tail_indexes:
331
+ cut = min(idx for idx in tail_indexes if idx >= 0)
332
+ head = pruned[:cut].rstrip()
333
+ pruned = f"{head}\n\n{_RUNTIME_TAIL_SUMMARY}" if head else _RUNTIME_TAIL_SUMMARY
334
+
335
+ # 压缩 Codex system prompt
336
+ if role == "system" and any(marker in pruned for marker in _CODEX_SYSTEM_MARKERS):
337
+ keep_sections: list[str] = []
338
+ intro_match = re.search(
339
+ r"^.*?(?=\n# AGENTS\.md spec|\n## Responsiveness|\n## Planning|\n## Task execution|\Z)",
340
+ pruned,
341
+ re.DOTALL,
342
+ )
343
+ if intro_match:
344
+ intro = intro_match.group(0).strip()
345
+ if intro:
346
+ keep_sections.append(intro)
347
+
348
+ # 提取 Codex 重要节(使用预编译正则)
349
+ for heading in ("## Personality", "# AGENTS.md spec"):
350
+ pattern = _CODEX_SECTION_PATTERNS[heading]
351
+ match = pattern.search(pruned)
352
+ if match:
353
+ section = match.group(0).strip()
354
+ if section:
355
+ keep_sections.append(section)
356
+
357
+ if keep_sections:
358
+ pruned = "\n\n".join(keep_sections)
359
+ else:
360
+ pruned = _CODEX_CORE_SUMMARY
361
+
362
+ # 压缩 harness user 消息
363
+ if role == "user" and _looks_like_harness_user_message(pruned):
364
+ if (
365
+ "# AGENTS.md instructions" in pruned
366
+ or "<environment_context>" in text
367
+ or "<skills_instructions>" in text
368
+ ):
369
+ return (
370
+ "Repository instructions and durable user context are provided. "
371
+ "Follow repository guidance while answering the user's actual request."
372
+ )
373
+
374
+ pruned = re.sub(r"\n{3,}", "\n\n", pruned).strip()
375
+ return pruned
376
+
377
+
378
+ def _compact_harness_message(role: str, content) -> str | None:
379
+ """把 Codex / Claude Code 注入的超长运行时提示压缩成短摘要,降低审核误伤。"""
380
+ text = _content_to_text(content)
381
+ if not text:
382
+ return None
383
+
384
+ if role == "system" and any(marker in text for marker in _CODEX_SYSTEM_MARKERS):
385
+ if "You are Claude Code" in text:
386
+ return (
387
+ "You are a coding assistant. Be precise, helpful, concise, and safe. "
388
+ "Use available tools when needed, follow repository instructions, and keep the user informed."
389
+ )
390
+ return (
391
+ "You are a coding assistant in Codex CLI. Be precise, helpful, concise, and safe. "
392
+ "Use available tools when needed, follow repository instructions, and keep the user informed."
393
+ )
394
+
395
+ if any(marker in text for marker in _PERMISSIONS_MARKERS):
396
+ return (
397
+ "Runtime permissions apply: filesystem access may be sandboxed, network may be restricted, "
398
+ "and some commands may require user approval."
399
+ )
400
+
401
+ if any(marker in text for marker in _SKILLS_MARKERS):
402
+ return (
403
+ "Runtime skill metadata is available. Use relevant skills only when explicitly requested or clearly applicable."
404
+ )
405
+
406
+ if role == "user" and _looks_like_harness_user_message(content):
407
+ return (
408
+ "Repository instructions and environment context are provided. Follow repository guidance "
409
+ "while answering the user's actual request."
410
+ )
411
+
412
+ return None
413
+
414
+
415
+ def _desensitize_tool_value(value: Any):
416
+ """递归处理 tool 定义的描述字段,插入零宽空格。"""
417
+ if isinstance(value, dict):
418
+ new_value = {}
419
+ for key, item in value.items():
420
+ if key in ("description", "title") and isinstance(item, str):
421
+ new_value[key] = desensitize_text(item)
422
+ else:
423
+ new_value[key] = _desensitize_tool_value(item)
424
+ return new_value
425
+ if isinstance(value, list):
426
+ return [_desensitize_tool_value(item) for item in value]
427
+ return value
428
+
429
+
430
+ def desensitize_messages(messages: Iterable[dict],
431
+ roles: tuple[str, ...] = ("system",),
432
+ desensitize_harness_user: bool = False,
433
+ compact_harness: bool = False) -> list[dict]:
434
+ """对指定角色的消息文本做脱敏,返回新的 messages 列表(不修改原对象)。
435
+
436
+ 默认只处理 system 角色(合规模板集中地)。可选处理 developer,
437
+ 以及 Codex 注入的 harness user 上下文;真实用户输入保持原样。
438
+ """
439
+ out: list[dict] = []
440
+ for m in messages:
441
+ if not isinstance(m, dict):
442
+ out.append(m)
443
+ continue
444
+
445
+ role = m.get("role")
446
+ should_desensitize = role in roles
447
+ if role == "user" and desensitize_harness_user:
448
+ should_desensitize = _looks_like_harness_user_message(m.get("content"))
449
+
450
+ nm = dict(m) # 浅拷贝,不污染调用方
451
+ if should_desensitize:
452
+ content = m.get("content")
453
+ compacted = _compact_harness_message(role, content) if compact_harness else None
454
+ if compacted is not None:
455
+ nm["content"] = desensitize_text(compacted)
456
+ elif isinstance(content, str):
457
+ nm["content"] = desensitize_text(_prune_runtime_fragments(role, content))
458
+ elif isinstance(content, list):
459
+ new_blocks = []
460
+ for blk in content:
461
+ if isinstance(blk, dict) and blk.get("type") == "text":
462
+ nb = dict(blk)
463
+ nb["text"] = desensitize_text(_prune_runtime_fragments(role, blk.get("text", "")))
464
+ new_blocks.append(nb)
465
+ else:
466
+ new_blocks.append(blk)
467
+ nm["content"] = new_blocks
468
+ out.append(nm)
469
+ return out
470
+
471
+
472
+ def desensitize_body(body: dict, roles: tuple[str, ...] = ("system",),
473
+ desensitize_harness_user: bool = False,
474
+ compact_harness: bool = False) -> dict:
475
+ """对请求体里的 messages 做脱敏,返回新的 body(浅拷贝)。
476
+
477
+ Args:
478
+ body: 请求体字典
479
+ roles: 需要脱敏的角色元组(默认只脱敏 system)
480
+ desensitize_harness_user: 是否脱敏 harness 用户消息
481
+ compact_harness: 是否压缩超长 harness 提示为短摘要
482
+
483
+ Returns:
484
+ 脱敏后的请求体(如有修改则为新字典,否则返回原字典)
485
+ """
486
+ if body.get("messages"):
487
+ nb = dict(body)
488
+ nb["messages"] = desensitize_messages(
489
+ body["messages"],
490
+ roles=roles,
491
+ desensitize_harness_user=desensitize_harness_user,
492
+ compact_harness=compact_harness,
493
+ )
494
+ return nb
495
+
496
+ return body
497
+
498
+
499
+ # ---------------------------------------------------------------------------
500
+ # 自测:python3 desensitize.py
501
+ # ---------------------------------------------------------------------------
502
+
503
+ if __name__ == "__main__":
504
+ samples = [
505
+ "Refuse requests for DoS attacks and exploit development.",
506
+ "Dual-use security tools (C2 frameworks, credential testing) require authorization.",
507
+ "这是一段正常的中文,不含任何触发词。",
508
+ "Prevent privilege escalation and brute force attacks.",
509
+ "No sensitive words here at all.",
510
+ ]
511
+ print("=== 脱敏前后对比 ===")
512
+ for s in samples:
513
+ d = desensitize_text(s)
514
+ changed = "✓改" if d != s else " 不"
515
+ print(f"{changed} | 原文: {s}")
516
+ if d != s:
517
+ print(f" | 脱敏: {d}")
518
+ print(f" | 可见字符相同,差异为零宽空格 U+200B")
519
+ print()
520
+ print("=== messages 脱敏(只处理 system)===")
521
+ msgs = [
522
+ {"role": "system", "content": "Refuse DoS attacks and exploit development."},
523
+ {"role": "user", "content": "explain DoS attacks"}, # 不应被改
524
+ ]
525
+ out = desensitize_messages(msgs)
526
+ for m in out:
527
+ print(f" [{m['role']}] {m['content']!r}")
528
+ print()
529
+ # 验证:脱敏后 system 改了,user 没改
530
+ assert "\u200b" in out[0]["content"], "system 应被脱敏"
531
+ assert "\u200b" not in out[1]["content"], "user 不应被脱敏"
532
+ print("✓ 自测通过:system 被脱敏,user 保持原样")