# ============================================================ # P3 偏差⑦⑧ 攻击面专项(Agent-N 验证员补网,全部确定性): # 偏差⑦ harness.refresh_confirmation_world_fingerprint 信封哈希同步重算 # —— 攻击面:第二张及以后确认卡的旧信封伪造/篡改/重放必须被拒。 # 偏差⑧ S6 兄弟卡世界指纹重锚 _reanchor_s6_sibling_cards # —— 攻击面:非兄弟(跨 run / 审批窗口内业务)改动不得借重锚混过漂移检测。 # 手法与 test_fallback_highrisk.py 一致:fake runner / FakeStore / tmp 隔离。 # ============================================================ from __future__ import annotations import copy import json from pathlib import Path from typing import ClassVar import pytest from server.agent_core import fallback_lane, harness from server.agent_core.providers import reset_provider from server.aps_domain.workflow import execute_confirmed from server.contracts import IntentResult from server.state.checkpoints import CheckpointStore from server.state.seed import seed_world DEMO_PRODUCT = "CTRL-A" @pytest.fixture(autouse=True) def _isolate(tmp_path, monkeypatch): monkeypatch.setenv("APS_FALLBACK_DIR", str(tmp_path / "fb")) monkeypatch.setenv("APS_FEATURES_PATH", str(tmp_path / "features.json")) monkeypatch.setenv("APS_FALLBACK_HIGHRISK_PATH", str(tmp_path / "fallback-highrisk.json")) monkeypatch.delenv("LLM_API_KEY", raising=False) monkeypatch.delenv("LLM_BASE_URL", raising=False) monkeypatch.delenv("LLM_MODEL", raising=False) monkeypatch.delenv("LLM_PROVIDER", raising=False) reset_provider() from server.state import checkpoints as _cp_mod from server.state import store as _store_mod kept_scoped = dict(_store_mod._stores) kept_cps = dict(_cp_mod._checkpoints) yield _store_mod._stores.clear() _store_mod._stores.update(kept_scoped) _cp_mod._checkpoints.clear() _cp_mod._checkpoints.update(kept_cps) reset_provider() class FakeStore: _KIND_TABLE: ClassVar[dict[str, str]] = { "salesOrder": "salesOrders", "material": "materials", "audit": "auditEvents", "importBatch": "importBatches"} def __init__(self, tmp_path: Path): self.data = seed_world() self._counters: dict[str, int] = {} self.tenant_uuid = "platform" self.world_key = "default" self.path = str(tmp_path / "world.json") self.checkpoints = CheckpointStore(str(tmp_path / "checkpoints.json")) def next_id(self, kind: str) -> int: if kind not in self._counters: table = self._KIND_TABLE.get(kind) self._counters[kind] = max( (x.get("id", 0) for x in self.data.get(table, []) if isinstance(x.get("id"), int)), default=0) if table else 0 self._counters[kind] += 1 return self._counters[kind] def save(self) -> None: pass def restore(self, world: dict) -> None: self.data = copy.deepcopy(world) self._counters.clear() def _cfg(tmp_path: Path) -> fallback_lane.FallbackConfig: return fallback_lane.FallbackConfig(pi_home=str(tmp_path / "pi-home")) def _write_features(tmp_path: Path) -> None: (tmp_path / "features.json").write_text( json.dumps({"version": 1, "features": {"fallback": True}}, ensure_ascii=False), encoding="utf-8") def _write_whitelist(tmp_path: Path) -> None: doc = {"whitelistVersion": 1, "updatedAt": "2026-09-05T10:00:00", "updatedBy": "attack-test", "scenarios": { "S4": {"enabled": True, "intents": ["flex.simulate_due", "flex.compare", "scenario.compare", "scenario.sensitivity"], "roles": ["planner", "admin"]}, "S6": {"enabled": True, "intents": ["mes.report"], "roles": ["planner", "admin"]}, "S7": {"enabled": True, "intents": ["agent.fallback.ops.config.apply", "agent.fallback.policy.update"], "roles": ["ops", "admin"]}}} (tmp_path / "fallback-highrisk.json").write_text( json.dumps(doc, ensure_ascii=False), encoding="utf-8") def _intent(query: str) -> IntentResult: return IntentResult(intent="unknown", params={"query": query}, confidence=0.1, source="LLM") def _fp(world: dict) -> str: return harness.world_fingerprint(world) def _pending(confirm_id: str) -> dict: return harness._approval_store.pending[confirm_id] def _make_plan_runner(plan_builder): def runner(task: str, work_dir: Path): run_dir = work_dir.parent plan = plan_builder(run_dir) (run_dir / "outbox").mkdir(parents=True, exist_ok=True) (run_dir / "outbox" / "plan.json").write_text( json.dumps(plan, ensure_ascii=False), encoding="utf-8") yield {"type": "message_end", "message": {"role": "assistant", "stopReason": "stop", "content": [{"type": "text", "text": "ok"}]}} yield {"type": "agent_end", "messages": []} return runner async def _stage(store: FakeStore, tmp_path: Path, plan: dict, query: str): reply = await fallback_lane.propose_reply( store, "s1", _intent(query), runner=_make_plan_runner(lambda rd: plan), config=_cfg(tmp_path)) confirm_ids = [b.props["confirmId"] for b in (reply.blocks or []) if getattr(b, "type", "") == "confirm-card"] if reply else [] return reply, confirm_ids def _approve(store: FakeStore, confirm_id: str, *, approve: bool = True) -> str: return execute_confirmed(store, confirm_id, approve=approve, actor="attacker-test") def _seed_s6_world(store: FakeStore, items: list[tuple]) -> None: store.data.setdefault("workOrders", []) store.data.setdefault("mesLinks", []) for wo_id, ext_id, status, pct, qty in items: store.data["workOrders"].append({ "id": wo_id, "mesExternalId": ext_id, "status": status, "progressPct": pct, "qtyDone": qty, "productCode": DEMO_PRODUCT}) store.data["mesLinks"].append({ "kind": "dispatch", "woId": wo_id, "externalWoId": ext_id, "idemKey": f"idem-{wo_id}"}) def _seed_mes_mirror(tmp_path: Path, monkeypatch, count: int): from server.integrations import mes_stub from server.integrations.mes_stub import MockMesClient client = MockMesClient(tmp_path / "mes_mirror.json") monkeypatch.setattr(mes_stub, "_client", client) for i in range(count): client.create_work_order({"productCode": DEMO_PRODUCT}, idem_key=f"idem-m-{i}") return client def _s6_plan(items: list[dict]) -> dict: return {"planVersion": 1, "scenario": "S6", "goal": "MES 断连补录", "steps": [{"seq": i + 1, "mode": "frozen", "intent": "mes.report", "summary": f"补录工单 {p['woId']}", "params": p, "constraints": {}, "expected": []} for i, p in enumerate(items)]} def _s3_plan() -> dict: rows = [{"customerName": "锐扬精密", "productCode": DEMO_PRODUCT, "quantity": 10, "deliveryDate": "2026-09-20", "orderNo": "RY-9001"}] return {"planVersion": 1, "scenario": "S3", "goal": "导入订单", "steps": [{"seq": 1, "mode": "frozen", "intent": "import.commit", "summary": "导入订单批 1 行", "params": {"batches": [{"kind": "orders", "rows": rows}]}, "constraints": {"kinds": ["orders"], "maxRows": 500}, "expected": [{"table": "salesOrders", "added": 1}]}]} def _setup(tmp_path: Path, monkeypatch, wo_ids=(9001, 9002)): _write_features(tmp_path) _write_whitelist(tmp_path) store = FakeStore(tmp_path) _seed_mes_mirror(tmp_path, monkeypatch, len(wo_ids)) _seed_s6_world(store, [ (w, f"MES-WO-{i + 1:04d}", "RUNNING", 0, 0) for i, w in enumerate(wo_ids)]) return store # --------------------------------------------------------------------------- # 偏差⑦ A-1:第二张确认卡信封完整(核心回归——修复前第二张卡 100% 信封自破) # --------------------------------------------------------------------------- async def test_a71_second_card_envelope_intact_after_refresh(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch) items = [{"woId": 9001, "progressPct": 80, "track": "fixed"}, {"woId": 9002, "progressPct": 60, "track": "fixed"}] _reply, confirm_ids = await _stage( store, tmp_path, _s6_plan(items), "MES 连不上了,今天的报工帮我补一下") assert len(confirm_ids) == 2 # 两张卡的信封在出卡指纹推进后都必须通过完整性校验 # (修复前:第二张卡 stage 时捕获到非空 beforeFingerprint,refresh 推进后 # envelopeHash 冻结 → verify_confirmation_envelope 必抛 PermissionError) for cid in confirm_ids: harness.verify_confirmation_envelope(_pending(cid)) # 不抛即过 # 先批第二张(乱序审批——S6 逐笔独立语义的直接压力面) msg2 = _approve(store, confirm_ids[1]) assert "执行完成" in msg2 wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9002]["progressPct"] == 60 # 再批第一张(兄弟重锚后信封仍须完整) harness.verify_confirmation_envelope(_pending(confirm_ids[0])) msg1 = _approve(store, confirm_ids[0]) assert "执行完成" in msg1 assert wos[9001]["progressPct"] == 80 # --------------------------------------------------------------------------- # 偏差⑦ A-2:伪造指纹推进(直改 beforeFingerprint 不重算信封)→ 信封拒绝 # --------------------------------------------------------------------------- async def test_a72_forged_fingerprint_advance_rejected(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch, wo_ids=(9001,)) _reply, confirm_ids = await _stage( store, tmp_path, _s6_plan([{"woId": 9001, "progressPct": 80, "track": "fixed"}]), "MES 连不上了帮我补录") assert len(confirm_ids) == 1 cid = confirm_ids[0] fp_before = _fp(store.data) # 攻击:绕过 refresh_confirmation_world_fingerprint,直接改写冻结指纹并落盘 # (等价于直接篡改 approvals.json——decide 每次事务从磁盘刷新,纯内存篡改 # 会被刷新抹掉,本身就是一层防御;这里按真实攻击面改盘) _pending(cid)["beforeFingerprint"] = "0" * 64 harness._approval_store.save() with pytest.raises(PermissionError, match="完整性校验失败"): harness.verify_confirmation_envelope(_pending(cid)) message = _approve(store, cid) assert "确认信封证据校验未通过" in message assert _fp(store.data) == fp_before # 零写入 wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9001]["progressPct"] == 0 # --------------------------------------------------------------------------- # 偏差⑦ A-3:篡改冻结参数(progressPct 80→100)→ 参数摘要拒绝 # --------------------------------------------------------------------------- async def test_a73_tampered_params_rejected(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch, wo_ids=(9001,)) _reply, confirm_ids = await _stage( store, tmp_path, _s6_plan([{"woId": 9001, "progressPct": 80, "track": "fixed"}]), "MES 连不上了帮我补录") cid = confirm_ids[0] fp_before = _fp(store.data) _pending(cid)["params"]["plan"]["steps"][0]["params"]["progressPct"] = 100 harness._approval_store.save() # 篡改落盘(真实攻击面) with pytest.raises(PermissionError, match="参数摘要不一致"): harness.verify_confirmation_envelope(_pending(cid)) message = _approve(store, cid) assert "确认信封证据校验未通过" in message assert _fp(store.data) == fp_before wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9001]["progressPct"] == 0 # 未按篡改值落账 # --------------------------------------------------------------------------- # 偏差⑦ A-4:重放已执行的确认卡 → 明确失效,不重复落账 # --------------------------------------------------------------------------- async def test_a74_replay_executed_card_rejected(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch, wo_ids=(9001,)) _reply, confirm_ids = await _stage( store, tmp_path, _s6_plan([{"woId": 9001, "progressPct": 80, "track": "fixed"}]), "MES 连不上了帮我补录") cid = confirm_ids[0] msg = _approve(store, cid) assert "执行完成" in msg wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9001]["progressPct"] == 80 replay = _approve(store, cid) # 同 confirmId 重放 assert "已失效" in replay assert wos[9001]["progressPct"] == 80 # 未重复落账 exec_audits = [e for e in store.data.get("auditEvents", []) if e.get("action") == "agent.fallback.execute" and e.get("category") == "WORLD_WRITE" and e.get("result") == "SUCCESS"] assert len(exec_audits) == 1 # 只有首次执行的成功总账 # --------------------------------------------------------------------------- # 偏差⑧ B-1:重锚只认同 run 兄弟卡——跨 run 卡指纹不推进,漂移检测照常拦 # --------------------------------------------------------------------------- async def test_b81_reanchor_only_touches_same_run_siblings(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch, wo_ids=(9001, 9002, 9003)) # run A:两笔补录 _reply_a, cards_a = await _stage( store, tmp_path, _s6_plan([{"woId": 9001, "progressPct": 80, "track": "fixed"}, {"woId": 9002, "progressPct": 60, "track": "fixed"}]), "MES 连不上了,今天的报工帮我补一下") # run B:另一轮补录(跨 run 非兄弟) _reply_b, cards_b = await _stage( store, tmp_path, _s6_plan([{"woId": 9003, "progressPct": 50, "track": "fixed"}]), "MES 连不上了,今天的报工帮我补一下") assert len(cards_a) == 2 and len(cards_b) == 1 run_a = _pending(cards_a[0])["params"]["runId"] run_b = _pending(cards_b[0])["params"]["runId"] assert run_a != run_b fp_b_frozen = _pending(cards_b[0])["beforeFingerprint"] msg_a0 = _approve(store, cards_a[0]) # 执行 A 第一笔 assert "执行完成" in msg_a0 # 兄弟卡 A2 被重锚:指纹推进到当前世界且信封完整 → 可正常批准 assert _pending(cards_a[1])["beforeFingerprint"] == _fp(store.data) harness.verify_confirmation_envelope(_pending(cards_a[1])) msg_a1 = _approve(store, cards_a[1]) assert "执行完成" in msg_a1 # 跨 run 卡 B 未被重锚(指纹停留原值)→ 漂移检测拦截,fail-closed assert _pending(cards_b[0])["beforeFingerprint"] == fp_b_frozen assert _pending(cards_b[0])["beforeFingerprint"] != _fp(store.data) msg_b = _approve(store, cards_b[0]) assert "世界指纹漂移" in msg_b wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9003]["progressPct"] == 0 # B 未落账 assert wos[9001]["progressPct"] == 80 # A 已落账不受影响 denied = [e for e in store.data.get("auditEvents", []) if e.get("action") == "agent.fallback.execute" and e.get("category") == "WORLD_WRITE" and e.get("result") == "DENIED"] assert denied # 漂移拒绝有 DENIED 审计 # --------------------------------------------------------------------------- # 偏差⑧ B-2(核心攻击):重锚后审批窗口内的非兄弟业务改动仍被漂移检测拦下 # --------------------------------------------------------------------------- async def test_b82_non_sibling_business_change_still_blocked(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch) items = [{"woId": 9001, "progressPct": 80, "track": "fixed"}, {"woId": 9002, "progressPct": 60, "track": "fixed"}] _reply, confirm_ids = await _stage( store, tmp_path, _s6_plan(items), "MES 连不上了,今天的报工帮我补一下") assert len(confirm_ids) == 2 msg1 = _approve(store, confirm_ids[0]) # 第一笔落账 → 兄弟重锚 assert "执行完成" in msg1 assert _pending(confirm_ids[1])["beforeFingerprint"] == _fp(store.data) # 攻击/并发事实:重锚之后、第二笔批准之前,发生非兄弟业务改动 # (另一用户的正常业务写——若借重锚混过检测,第二笔将按过时口径落账) store.data.setdefault("salesOrders", []).append( {"id": 777001, "orderNo": "ATTACK-WINDOW", "productCode": DEMO_PRODUCT, "quantity": 1, "deliveryDate": "2026-09-25"}) store.save() msg2 = _approve(store, confirm_ids[1]) assert "世界指纹漂移" in msg2 # 被拦下:重锚没有放行非兄弟改动 wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9002]["progressPct"] == 0 # 第二笔未落账 assert wos[9001]["progressPct"] == 80 # 第一笔已落账不动 # --------------------------------------------------------------------------- # 偏差⑧ B-3:伪造兄弟(篡改 runId 蹭重锚)→ 参数摘要破损,信封层拒绝 # --------------------------------------------------------------------------- async def test_b83_forged_sibling_runid_rejected(tmp_path, monkeypatch): store = _setup(tmp_path, monkeypatch) _reply_a, cards_a = await _stage( store, tmp_path, _s6_plan([{"woId": 9001, "progressPct": 80, "track": "fixed"}, {"woId": 9002, "progressPct": 60, "track": "fixed"}]), "MES 连不上了,今天的报工帮我补一下") _reply_b, cards_b = await _stage( store, tmp_path, _s3_plan(), "把这份客户表格导进来") assert len(cards_a) == 2 and len(cards_b) == 1 run_a = _pending(cards_a[0])["params"]["runId"] orders_before = len(store.data.get("salesOrders") or []) # 攻击:把非兄弟卡(S3 导入卡)的 runId 篡改为 S6 run 并落盘,试图让重锚 # 免费推进它的冻结指纹、混过漂移检测 _pending(cards_b[0])["params"]["runId"] = run_a harness._approval_store.save() msg_a0 = _approve(store, cards_a[0]) # 触发重锚 assert "执行完成" in msg_a0 # 重锚按匹配条件确实推进了被篡改卡(机制如实运作)…… assert _pending(cards_b[0])["beforeFingerprint"] == _fp(store.data) # ……但 params 被篡改 → paramsHash 破损,信封层物理拒绝 msg_b = _approve(store, cards_b[0]) assert "确认信封证据校验未通过" in msg_b assert len(store.data.get("salesOrders") or []) == orders_before # 导入未执行 # 真兄弟卡不受伪造事件影响,仍可正常批准落账 msg_a1 = _approve(store, cards_a[1]) assert "执行完成" in msg_a1 wos = {w["id"]: w for w in store.data["workOrders"]} assert wos[9002]["progressPct"] == 60