修复: 阻断目标事实改写造成的回放泄露
This commit is contained in:
parent
843d5d77d3
commit
96ebbc1d4c
@ -36,7 +36,7 @@ disable-model-invocation: true
|
|||||||
|
|
||||||
授权、来源版本、目标章禁读或内容级泄露检查任一失败,都必须 fail-closed,返回明确的阻断状态,不靠重试绕过。
|
授权、来源版本、目标章禁读或内容级泄露检查任一失败,都必须 fail-closed,返回明确的阻断状态,不靠重试绕过。
|
||||||
|
|
||||||
`run_replay.py` 会同时审计公共快照和三臂卡注入区。没有 `leakageAudit` 配置、发现未来章记录或目标事实内容匹配时,不生成 `snapshot_manifest`,也不进入 planner。
|
`run_replay.py` 会同时审计公共快照和三臂卡注入区。没有 `leakageAudit` 配置、发现未来章记录、目标事实完整匹配,或原子事件子句达到多强锚点与覆盖率门时,不生成 `snapshot_manifest`,也不进入 planner。
|
||||||
|
|
||||||
## 产物边界
|
## 产物边界
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ disable-model-invocation: true
|
|||||||
- planner、detector、judge 子进程统一受 `--timeout-seconds` 限制,默认 300 秒;任一超时分别落盘 `planner_timeout`、`detector_timeout`、`judge_timeout`,不得继续进入后续阶段或标记 `completed`。
|
- planner、detector、judge 子进程统一受 `--timeout-seconds` 限制,默认 300 秒;任一超时分别落盘 `planner_timeout`、`detector_timeout`、`judge_timeout`,不得继续进入后续阶段或标记 `completed`。
|
||||||
- `scripts/write_report.py`:从 `run_result.json` 生成独立严格 schema 的安全摘要,只接受受限标识符、枚举、数字、短安全摘要和 SHA-256;不会读取候选正文,也不会把候选路径以外的原始响应写入报告。
|
- `scripts/write_report.py`:从 `run_result.json` 生成独立严格 schema 的安全摘要,只接受受限标识符、枚举、数字、短安全摘要和 SHA-256;不会读取候选正文,也不会把候选路径以外的原始响应写入报告。
|
||||||
|
|
||||||
真实作品运行前必须先从权威来源取得不可变授权快照。当前状态:DDL 已实现待 apply,真实记录未写,实跑未开始。用户授权只能登记为 `research_only` 且 `allowedPurpose=["offline_evaluation"]`,不得伪造为 `licensed`;缺记录继续保持 `blocked_authorization`。
|
真实作品运行前必须先从权威来源取得不可变授权快照。当前 work=8 已有 `research_only` 且 `allowedPurpose=["offline_evaluation"]` 的有效记录;真实 489 dry-run 因安若雪卡含错标目标事件被 `invalid_snapshot` 阻断,尚未进入 planner。其他作品缺记录时继续保持 `blocked_authorization`,不得伪造为 `licensed`。
|
||||||
|
|
||||||
## 正文 A/B/C 回放
|
## 正文 A/B/C 回放
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import difflib
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
from typing import Any, Iterator, Mapping
|
from typing import Any, Iterator, Mapping
|
||||||
|
|
||||||
from build_snapshot import normalize_chapter, normalize_chapter_range, sha256_value
|
from build_snapshot import normalize_chapter, normalize_chapter_range, sha256_value
|
||||||
@ -21,6 +24,9 @@ STATUS_INVALID_INPUT = "invalid_audit_input"
|
|||||||
_CHAPTER_KEYS = frozenset(
|
_CHAPTER_KEYS = frozenset(
|
||||||
{"chapter", "chapter_no", "order_no", "章", "章号", "from_order", "to_order"}
|
{"chapter", "chapter_no", "order_no", "章", "章号", "from_order", "to_order"}
|
||||||
)
|
)
|
||||||
|
_FACT_SEGMENT_SPLIT = re.compile(r"[,,;;。!?!?、\n]+")
|
||||||
|
_FACT_LABEL_PREFIX = re.compile(r"^(?:章目标|关键事件|出场角色|伏笔动作|章末钩子)[::]")
|
||||||
|
_NON_FACT_CHARACTER = re.compile(r"[^0-9a-z\u3400-\u9fff]+")
|
||||||
|
|
||||||
|
|
||||||
def _chapter_bounds(record: Mapping[str, Any]) -> tuple[int, int] | None:
|
def _chapter_bounds(record: Mapping[str, Any]) -> tuple[int, int] | None:
|
||||||
@ -113,6 +119,54 @@ def _finding(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_fact_text(value: str) -> str:
|
||||||
|
"""统一宽窄字符并移除标点,只保留可稳定比较的中文、字母和数字。"""
|
||||||
|
|
||||||
|
normalized = unicodedata.normalize("NFKC", value).lower()
|
||||||
|
return _NON_FACT_CHARACTER.sub("", normalized)
|
||||||
|
|
||||||
|
|
||||||
|
def _target_fact_segments(text: str) -> list[str]:
|
||||||
|
"""把复合目标细纲拆成原子事件子句,避免只能逐字匹配整段长文本。"""
|
||||||
|
|
||||||
|
segments: list[str] = []
|
||||||
|
for raw_segment in _FACT_SEGMENT_SPLIT.split(text):
|
||||||
|
stripped = _FACT_LABEL_PREFIX.sub("", raw_segment.strip())
|
||||||
|
normalized = _normalize_fact_text(stripped)
|
||||||
|
# 太短的角色名或栏目词缺乏事件区分度,不能单独触发失败关闭。
|
||||||
|
if len(normalized) >= 6:
|
||||||
|
segments.append(normalized)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def _has_partial_target_fact(value: str, fact_text: str) -> bool:
|
||||||
|
"""识别目标事件的局部改写,同时用多锚点阈值控制历史事实误报。"""
|
||||||
|
|
||||||
|
normalized_value = _normalize_fact_text(value)
|
||||||
|
if len(normalized_value) < 6:
|
||||||
|
return False
|
||||||
|
for segment in _target_fact_segments(fact_text):
|
||||||
|
if segment in normalized_value:
|
||||||
|
return True
|
||||||
|
blocks = [
|
||||||
|
block
|
||||||
|
for block in difflib.SequenceMatcher(
|
||||||
|
None, segment, normalized_value, autojunk=False
|
||||||
|
).get_matching_blocks()
|
||||||
|
if block.size >= 3
|
||||||
|
]
|
||||||
|
# 三个互不重叠的强锚点可表达“实体 + 能力/动作 + 数值/结果”的同一事件;
|
||||||
|
# 两个常见词(例如角色名 + 同步率)不足以判定泄露。
|
||||||
|
matched_characters = sum(block.size for block in blocks)
|
||||||
|
if (
|
||||||
|
len(blocks) >= 3
|
||||||
|
and matched_characters >= 9
|
||||||
|
and matched_characters / len(segment) >= 0.5
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def audit_snapshot(
|
def audit_snapshot(
|
||||||
snapshot: Mapping[str, Any],
|
snapshot: Mapping[str, Any],
|
||||||
target_facts: Mapping[str, Any],
|
target_facts: Mapping[str, Any],
|
||||||
@ -172,6 +226,15 @@ def audit_snapshot(
|
|||||||
findings.append(
|
findings.append(
|
||||||
_finding(reason="target_fact_match", path=path, value=value, fact=fact)
|
_finding(reason="target_fact_match", path=path, value=value, fact=fact)
|
||||||
)
|
)
|
||||||
|
elif _has_partial_target_fact(value, fact["text"]):
|
||||||
|
findings.append(
|
||||||
|
_finding(
|
||||||
|
reason="target_fact_partial_match",
|
||||||
|
path=path,
|
||||||
|
value=value,
|
||||||
|
fact=fact,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": STATUS_INVALID_SNAPSHOT if findings else STATUS_READY,
|
"status": STATUS_INVALID_SNAPSHOT if findings else STATUS_READY,
|
||||||
|
|||||||
@ -65,6 +65,87 @@ class LeakageAuditTest(unittest.TestCase):
|
|||||||
self.assertIn("factHash", result["findings"][0])
|
self.assertIn("factHash", result["findings"][0])
|
||||||
self.assertIn("valueHash", result["findings"][0])
|
self.assertIn("valueHash", result["findings"][0])
|
||||||
|
|
||||||
|
def test_paraphrased_real_target_event_is_blocked(self):
|
||||||
|
"""真实错标卡不会逐字复制目标细纲,仍须识别同一事件的多个强锚点。"""
|
||||||
|
|
||||||
|
facts = {
|
||||||
|
"targetChapter": 489,
|
||||||
|
"forbiddenFacts": [
|
||||||
|
{
|
||||||
|
"id": "key-events-489",
|
||||||
|
"firstChapter": 489,
|
||||||
|
"text": "关键事件:苏铭投两枚核雷撕开缺口、安若雪同步率100%强攻加特朗、铎叔反水贯杀加特朗",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = audit_snapshot(
|
||||||
|
{
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"chapter": 488,
|
||||||
|
"fact": "与加特朗激战中深陷压力,转交主攻位置后用生命进发让同步率飙至100%,击碎加特朗能量屏障",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
facts,
|
||||||
|
as_of=488,
|
||||||
|
target=489,
|
||||||
|
)
|
||||||
|
encoded = json.dumps(result, ensure_ascii=False)
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["findings"][0]["reason"], "target_fact_partial_match")
|
||||||
|
self.assertNotIn("加特朗", encoded)
|
||||||
|
self.assertNotIn("同步率", encoded)
|
||||||
|
|
||||||
|
def test_related_but_distinct_history_does_not_false_positive(self):
|
||||||
|
"""只共享旧角色和体系术语、但缺少目标事件关键锚点时不得误报。"""
|
||||||
|
|
||||||
|
facts = {
|
||||||
|
"targetChapter": 489,
|
||||||
|
"forbiddenFacts": [
|
||||||
|
{
|
||||||
|
"id": "key-events-489",
|
||||||
|
"firstChapter": 489,
|
||||||
|
"text": "安若雪同步率100%强攻加特朗",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = audit_snapshot(
|
||||||
|
{"events": [{"chapter": 387, "fact": "安若雪开启拘束解除,同步率突破95%重创死亡收割者"}]},
|
||||||
|
facts,
|
||||||
|
as_of=488,
|
||||||
|
target=489,
|
||||||
|
)
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertEqual(result["findingCount"], 0)
|
||||||
|
|
||||||
|
def test_entities_scattered_across_long_history_do_not_form_one_event(self):
|
||||||
|
"""多个实体分散在旧细纲的不同事件里,不能仅凭名字凑成目标事件。"""
|
||||||
|
|
||||||
|
facts = {
|
||||||
|
"targetChapter": 489,
|
||||||
|
"forbiddenFacts": [
|
||||||
|
{
|
||||||
|
"id": "network-attack-489",
|
||||||
|
"firstChapter": 489,
|
||||||
|
"text": "赛莉丝入虚拟城外圈夺控防御闸门并坑杀凯多萨康里弗两方",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
historical_outline = (
|
||||||
|
"佛罗科密授康里弗追求安若雪以图谋王位;"
|
||||||
|
"赛莉丝入侵要塞惊见囚禁的高等级灵械后退出;"
|
||||||
|
"另一场景中凯多萨与康里弗争执,搅乱苏铭和安若雪的会面。"
|
||||||
|
)
|
||||||
|
result = audit_snapshot(
|
||||||
|
{"chapters": [{"chapter": 484, "outline": historical_outline}]},
|
||||||
|
facts,
|
||||||
|
as_of=488,
|
||||||
|
target=489,
|
||||||
|
)
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertEqual(result["findingCount"], 0)
|
||||||
|
|
||||||
def test_target_and_as_of_must_form_next_chapter_pair(self):
|
def test_target_and_as_of_must_form_next_chapter_pair(self):
|
||||||
result = audit_snapshot({}, target_facts(), as_of=487, target=489)
|
result = audit_snapshot({}, target_facts(), as_of=487, target=489)
|
||||||
self.assertFalse(result["ok"])
|
self.assertFalse(result["ok"])
|
||||||
|
|||||||
@ -8,17 +8,17 @@
|
|||||||
|
|
||||||
**技术栈:** `agent-example` 文件版实验台、PostgreSQL `muse-example` 只读查询、现有 `planning`/`read-context`/`detect`/`quality-gate` skill、Opus 规划与评审子代理;确定性冻结和校验脚本使用仓库 `.venv` 的 Python。
|
**技术栈:** `agent-example` 文件版实验台、PostgreSQL `muse-example` 只读查询、现有 `planning`/`read-context`/`detect`/`quality-gate` skill、Opus 规划与评审子代理;确定性冻结和校验脚本使用仓库 `.venv` 的 Python。
|
||||||
|
|
||||||
## 执行状态(2026-07-19)
|
## 执行状态(2026-07-20)
|
||||||
|
|
||||||
- 已落地并同步到 `agent-example/main`:`fbb262c`(冻结/细纲/评分合同)、`a54a3a4`(审查反馈收紧与回放编排)、`1243d9b`(外部 planner execute 链路测试)。
|
- 已合入 `agent-example/main`:冻结/细纲/评分合同、三臂 planner、逐候选 detector、双 judge 反序盲评、稳定性门、原文件授权链和安全报告;集成提交为 `843d5d7`。
|
||||||
- 已验证:replay-eval 70 个离线测试、fine-outline 合同 3 个测试全部通过;fake runner 已跑通三臂 planner、真正不识别臂卡内容的逐候选 detector、双 judge 反序盲评、子进程超时失败关闭、稳定性门和去盲矩阵。测试只使用合成 fixture,不代表参考作品评测结果。
|
- 已验证:replay-eval 127 个离线测试全部通过;fake runner 已跑通三臂 planner、真正不识别臂卡内容的逐候选 detector、双 judge 反序盲评、子进程超时失败关闭、稳定性门和去盲矩阵。合成 fixture 不代表参考作品评测结果。
|
||||||
- 已新增并验证:`audit_leakage.py` 对公共快照和三臂卡注入区执行内容级事实审计;`load_reference_work.py` 通过 PostgreSQL 只读事务组装仓库外临时配置,候选卡标记为 `eval_draft`,不进入生产检索。
|
- 已新增并验证:`audit_leakage.py` 对公共快照和三臂卡注入区执行内容级事实审计;`load_reference_work.py` 通过 PostgreSQL 只读事务组装仓库外临时配置,候选卡标记为 `eval_draft`,不进入生产检索。
|
||||||
- 已执行真实适配 smoke:work=8、冻结 488、目标 489,组装 31 个完整历史大纲窗、6 个近章细纲摘要、正确/错配卡各 2 张;送入回放仍为 `blocked_authorization`(`example_reference_work` 没有授权快照),未生成 `snapshot_manifest`,未调用模型。
|
- 授权表已应用,work=8 已有一条有效的 `research_only + offline_evaluation` 不可变快照;原文件、document SHA-256 和 `raw-file-v1` 来源版本三方一致,重验时间为 2026-08-18。
|
||||||
- 已执行真实数据库前置 smoke:深空之影 `work_id=8` 的 `example_reference_work` 表没有不可变授权快照/版权用途字段,结果为 `blocked_authorization`,三臂均未调用模型。
|
- 已执行真实 work=8 / as_of=488 / target=489 dry-run:正确卡选择为安若雪与圣蒂曼行星,placebo 为同型近体量的伊蕾莉雅与鹦鹉螺号;未调用模型。
|
||||||
- 任务 A 已实现原文件版本与 append-only 授权快照 DDL:work=8 的权威原文件 `document_id=7`、SHA-256=`02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4`,来源版本固定为 `raw-file-v1:sha256:<hash>`,不再使用作品 revision+导入章数。
|
- 首次真实 dry-run 暴露内容审计假阴性:安若雪卡把第 489 章“加特朗 / 同步率 100%”事件错标为第 488 章,旧审计只做完整字符串包含而错误放行。现已增加原子事件子句、多强锚点和目标子句覆盖率门,真实样本被正确阻断为 `invalid_snapshot`,不生成 manifest、不进入 planner。
|
||||||
- 当前状态:**DDL 已实现待 apply,真实记录未写,实跑未开始**。用户授权只能登记为 `research_only + offline_evaluation`,不得伪造 `licensed`;apply 与 INSERT 均不在本任务执行范围。
|
- 当前状态:**授权与真实装配已通,目标样本因上游卡章号错标被内容门阻断,模型实跑未开始**。必须先修正或隔离泄露来源,再执行三臂模型回放。
|
||||||
- `.claude/skills/llm/scripts/test_quota.py` 已统一到当前 `$24/6000` 契约:预算场景使用 `$24.5`,调用上限场景使用 `6000`,并增加策略常量断言。
|
- `.claude/skills/llm/scripts/test_quota.py` 已统一到当前 `$24/6000` 契约:预算场景使用 `$24.5`,调用上限场景使用 `6000`,并增加策略常量断言。
|
||||||
- 当前允许的结论是“冻结、变量控制、内容级泄露审计、候选结构门、detector/judge 双评编排、安全摘要机制和授权快照代码合同已通”;不能说数据库表已应用、真实授权已写入、细纲智能体或知识卡已通过。真实授权接入和 430/489/550 实样仍待完成。
|
- 当前允许的结论是“授权、真实数据装配、冻结、变量控制、内容级泄露阻断、候选结构门、detector/judge 双评编排和安全摘要机制已通”;不能说细纲智能体或知识卡已通过。489 样本需先清除泄露来源,430/550 样本仍待执行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -301,7 +301,7 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards}
|
|||||||
- [x] 将卡生成 `as_of=N` 规划视图,剔除终态字段、未来里程碑、无绝对上界条目和无法溯源字段。
|
- [x] 将卡生成 `as_of=N` 规划视图,剔除终态字段、未来里程碑、无绝对上界条目和无法溯源字段。
|
||||||
- [x] 预检授权双闸、来源状态、目标章禁读、候选卡选择来源和原书全文不落最终报告。
|
- [x] 预检授权双闸、来源状态、目标章禁读、候选卡选择来源和原书全文不落最终报告。
|
||||||
- [x] 用离线 fixture 覆盖未来里程碑、跨 N 窗口、章号归一化、嵌套章记录、终态裁剪、缺授权失败关闭和三臂公共区哈希一致。
|
- [x] 用离线 fixture 覆盖未来里程碑、跨 N 窗口、章号归一化、嵌套章记录、终态裁剪、缺授权失败关闭和三臂公共区哈希一致。
|
||||||
- [x] 增加目标章内容级事实泄露审计,发现错位事件或臂内卡未来记录时整例作废;审计结果只保留路径和哈希。
|
- [x] 增加目标章内容级事实泄露审计;完整匹配外,还按原子事件子句执行多强锚点与覆盖率判断,能阻断真实错标改写并避免长历史细纲实体散落造成的假阳性。审计结果只保留路径和哈希。
|
||||||
- [x] 增加 PostgreSQL 只读参考作品适配器;只读取结构化大纲/细纲摘要和预注册卡,目标章仅进入临时审计 proxy,缺授权不放行。
|
- [x] 增加 PostgreSQL 只读参考作品适配器;只读取结构化大纲/细纲摘要和预注册卡,目标章仅进入临时审计 proxy,缺授权不放行。
|
||||||
|
|
||||||
### Task 2:细纲智能体功能合同
|
### Task 2:细纲智能体功能合同
|
||||||
@ -329,7 +329,7 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards}
|
|||||||
|
|
||||||
- [x] 给 detector 增加细纲候选的检查对象、严重度和证据格式。
|
- [x] 给 detector 增加细纲候选的检查对象、严重度和证据格式。
|
||||||
- [x] 给 judge 增加 `fine_outline_replay` rubric profile,明确不评正文文风和文笔。
|
- [x] 给 judge 增加 `fine_outline_replay` rubric profile,明确不评正文文风和文笔。
|
||||||
- [x] 在回放编排中固定两个独立 judge、匿名 arm 和第二评委候选顺序反转;fake runner 已覆盖双评、rubric 合同和稳定性失败关闭,真实样本尚未越过授权门。
|
- [x] 在回放编排中固定两个独立 judge、匿名 arm 和第二评委候选顺序反转;fake runner 已覆盖双评、rubric 合同和稳定性失败关闭,真实样本当前被内容泄露门阻断。
|
||||||
- [x] 机械测试确保 rubric 不包含正文质量维度,且每个分数必须有证据字段。
|
- [x] 机械测试确保 rubric 不包含正文质量维度,且每个分数必须有证据字段。
|
||||||
|
|
||||||
### Task 4:回放编排与最小首跑
|
### Task 4:回放编排与最小首跑
|
||||||
@ -341,11 +341,11 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards}
|
|||||||
- Test: `.claude/skills/replay-eval/scripts/test_run_replay.py`
|
- Test: `.claude/skills/replay-eval/scripts/test_run_replay.py`
|
||||||
- Create: `docs/replay/<run-id>-summary.md`
|
- Create: `docs/replay/<run-id>-summary.md`
|
||||||
|
|
||||||
- [x] 先用合成 fixture 完成一个机制 dry-run,并用 fake planner 验证 execute 链路;真实样本尚未越过授权门。
|
- [x] 先用合成 fixture 完成机制 dry-run,并用 fake planner 验证 execute 链路;真实 489 样本已完成确定性 dry-run 并因内容泄露正确阻断。
|
||||||
- [x] 编排器支持每个目标章 A/B/C 三臂,planner 使用相同身份段、功能段、预算和输出合同;尚未运行参考作品目标章。
|
- [x] 编排器支持每个目标章 A/B/C 三臂,planner 使用相同身份段、功能段、预算和输出合同;参考作品目标章尚未启动模型。
|
||||||
- [x] 两个独立 judge 对每章候选做顺序互换盲评;detector 阻断样本不送 judge。当前由 fake runner 离线测试验证,尚无真实样本结果。
|
- [x] 两个独立 judge 对每章候选做顺序互换盲评;detector 阻断样本不送 judge。当前由 fake runner 离线测试验证,尚无真实样本结果。
|
||||||
- [x] 运行时原始候选和标准事实摘要只放仓库外临时目录;安全报告只写摘要、定位、评分入口、哈希和失败类别。
|
- [x] 运行时原始候选和标准事实摘要只放仓库外临时目录;安全报告只写摘要、定位、评分入口、哈希和失败类别。
|
||||||
- [x] 接入真实数据库只读适配 smoke;授权缺失时维持 `blocked_authorization`,不启动 planner。
|
- [x] 接入真实数据库只读适配与授权快照;授权、来源或内容审计任一失败都不启动 planner。
|
||||||
- [ ] 产出真实卡增量矩阵和样本级假阴/假阳说明;不能用机制 smoke 的合成结果代替。
|
- [ ] 产出真实卡增量矩阵和样本级假阴/假阳说明;不能用机制 smoke 的合成结果代替。
|
||||||
|
|
||||||
### Task 5:滚动评估门
|
### Task 5:滚动评估门
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user