ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,279 @@
"""
基于规则(正则)的离线日志脱敏引擎
与 agent.py 中依赖本地 LLM 的方案互补:本模块不需要任何模型或网络,
纯靠正则表达式 + 校验算法(Luhn、身份证校验码)识别日志 / 工具输出中的
敏感信息,速度快、结果确定,适合作为 Agent 日志落盘前的第一道防线。
覆盖的敏感信息类别(按匹配优先级从高到低):
- 私钥 / 证书(PEM 块)
- JWT
- 云厂商与第三方密钥(AWS AKIA、GitHub、Slack、Google、OpenAI 风格 sk-
- HTTP Authorization: Bearer / Basic 令牌
- 配置中的口令 / 密钥赋值(password=..., token: ... 等)
- 邮箱地址
- 信用卡号(Luhn 校验)
- IBAN 国际银行账号
- 美国社会安全号(SSN
- 中国大陆身份证号(校验码验证)
- 中国大陆手机号
- IPv4 地址
每一类都会被替换为带类别标签的占位符(如 [REDACTED_API_KEY]),
既隐去了原值,又保留了“这里原本是什么”的可读性,方便排障。
"""
import re
from collections import Counter
from typing import Dict, List, Tuple
def _luhn_ok(number: str) -> bool:
"""Luhn 校验,用于降低信用卡号的误报率"""
digits = [int(c) for c in number if c.isdigit()]
if not 13 <= len(digits) <= 19:
return False
checksum = 0
parity = len(digits) % 2
for i, d in enumerate(digits):
if i % 2 == parity:
d *= 2
if d > 9:
d -= 9
checksum += d
return checksum % 10 == 0
def _cn_id_ok(value: str) -> bool:
"""中国大陆二代身份证号(18 位)校验码验证"""
s = value.upper()
if len(s) != 18 or not s[:17].isdigit():
return False
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = "10X98765432"
total = sum(int(s[i]) * weights[i] for i in range(17))
return check_codes[total % 11] == s[17]
# 每条规则:(类别, 占位符, 编译后的正则, 用于取值的分组号, 可选校验函数)
# 分组号为 0 表示整段命中都要脱敏;为 N 表示只脱敏第 N 个捕获组(保留键名等上下文)。
_RULES = [
(
"private_key", "[REDACTED_PRIVATE_KEY]",
# Truncated PEM (BEGIN without END) must still redact through EOF.
re.compile(
r"-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----"
r"[\s\S]*?(?:-----END (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----|(?=\Z))"
),
0, None,
),
(
"jwt", "[REDACTED_JWT]",
re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"),
0, None,
),
(
# 连接串中的口令,如 postgres://user:PASSWORD@host:5432/db
"url_credential", "[REDACTED_URL_CRED]",
re.compile(r"://[^\s:/@]*:([^\s@]+)@"),
1, None,
),
(
"aws_access_key", "[REDACTED_AWS_KEY]",
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
0, None,
),
(
"github_token", "[REDACTED_GITHUB_TOKEN]",
re.compile(r"\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{20,})\b"),
0, None,
),
(
"slack_token", "[REDACTED_SLACK_TOKEN]",
re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}\b"),
0, None,
),
(
"google_api_key", "[REDACTED_GOOGLE_API_KEY]",
re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b"),
0, None,
),
(
"api_key", "[REDACTED_API_KEY]",
re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b"),
0, None,
),
(
"bearer_token", "[REDACTED_BEARER_TOKEN]",
re.compile(r"(?i)\bBearer\s+([A-Za-z0-9._~+/=-]{10,})"),
1, None,
),
(
"basic_auth", "[REDACTED_BASIC_AUTH]",
# Require Authorization: so English "Basic knowledge …" is not redacted.
re.compile(r"(?i)\bAuthorization\s*:\s*Basic\s+([A-Za-z0-9+/=]{4,})"),
1, None,
),
(
"secret_assignment", "[REDACTED_SECRET]",
re.compile(
r"(?i)(?:password|passwd|pwd|secret|token|api[_-]?key|"
r"access[_-]?key|auth|credential)[\"']?\s*[=:]\s*"
r"(?:\"([^\"]{4,})\"|'([^']{4,})'|([^\s\"',}]{4,}))"
),
(1, 2, 3), None,
),
(
"email", "[REDACTED_EMAIL]",
re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"),
0, None,
),
(
"credit_card", "[REDACTED_CREDIT_CARD]",
re.compile(r"\b(?:\d[ -]?){13,19}\b"),
0, _luhn_ok,
),
(
"iban", "[REDACTED_IBAN]",
re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b"),
0, None,
),
(
"us_ssn", "[REDACTED_SSN]",
re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
0, None,
),
(
"cn_id_card", "[REDACTED_ID_CARD]",
re.compile(r"\b\d{17}[\dXx]\b"),
0, _cn_id_ok,
),
(
"cn_phone", "[REDACTED_PHONE]",
re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)"),
0, None,
),
(
"ip_address", "[REDACTED_IP]",
re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b"),
0, None,
),
]
# 人类可读的类别中文名,用于打印汇总
CATEGORY_LABELS = {
"private_key": "私钥 / 证书",
"jwt": "JWT 令牌",
"url_credential": "连接串凭据",
"aws_access_key": "AWS 访问密钥",
"github_token": "GitHub 令牌",
"slack_token": "Slack 令牌",
"google_api_key": "Google API Key",
"api_key": "API Key (sk-)",
"bearer_token": "Bearer 令牌",
"basic_auth": "Basic 认证",
"secret_assignment": "口令 / 密钥赋值",
"email": "邮箱地址",
"credit_card": "信用卡号",
"iban": "IBAN 银行账号",
"us_ssn": "美国社保号(SSN)",
"cn_id_card": "身份证号",
"cn_phone": "手机号",
"ip_address": "IP 地址",
}
def sanitize(text: str) -> Tuple[str, List[Dict]]:
"""
对文本执行离线规则脱敏。
Returns:
- 脱敏后的文本
- 命中列表,每项为 {category, value, placeholder, start, end}
"""
candidates: List[Dict] = []
for priority, (category, placeholder, pattern, group, validator) in enumerate(_RULES):
groups = group if isinstance(group, tuple) else (group,)
for m in pattern.finditer(text):
start = end = -1
for g in groups:
start, end = m.span(g)
if start >= 0:
break
if start < 0: # 该捕获组未参与本次匹配
continue
value = text[start:end]
if validator and not validator(value):
continue
candidates.append({
"category": category,
"placeholder": placeholder,
"value": value,
"start": start,
"end": end,
"priority": priority,
})
# 处理重叠:优先级高(数字小)的规则胜出,避免同一段被重复/错误脱敏
candidates.sort(key=lambda c: (c["priority"], c["start"]))
accepted: List[Dict] = []
for c in candidates:
if any(not (c["end"] <= a["start"] or c["start"] >= a["end"]) for a in accepted):
continue
accepted.append(c)
# 按位置顺序重建脱敏文本
accepted.sort(key=lambda c: c["start"])
parts: List[str] = []
last = 0
for c in accepted:
parts.append(text[last:c["start"]])
parts.append(c["placeholder"])
last = c["end"]
parts.append(text[last:])
findings = [
{k: c[k] for k in ("category", "value", "placeholder", "start", "end")}
for c in accepted
]
return "".join(parts), findings
def summarize(findings: List[Dict]) -> Counter:
"""统计各类别命中次数"""
return Counter(f["category"] for f in findings)
def print_report(name: str, original: str, redacted: str, findings: List[Dict]) -> None:
"""打印单条样本的 before/after 与命中明细"""
print(f"\n{'=' * 64}")
print(f"样本: {name} (命中 {len(findings)} 处敏感信息)")
print("=" * 64)
print("--- 脱敏前 (BEFORE) ---")
print(original.rstrip())
print("\n--- 脱敏后 (AFTER) ---")
print(redacted.rstrip())
if findings:
print("\n--- 命中明细 ---")
for f in findings:
label = CATEGORY_LABELS.get(f["category"], f["category"])
print(f" [{label}] {f['value']} -> {f['placeholder']}")
if __name__ == "__main__":
# 直接运行本模块时,对内置样本做一次快速演示
from samples import SAMPLES
total = Counter()
for name, text in SAMPLES:
redacted, findings = sanitize(text)
print_report(name, text, redacted, findings)
total.update(summarize(findings))
print(f"\n{'=' * 64}")
print("脱敏类别汇总")
print("=" * 64)
for category, count in total.most_common():
label = CATEGORY_LABELS.get(category, category)
print(f" {label:<16} {count}")
print(f"\n 合计脱敏 {sum(total.values())} 处敏感信息")