aps-agent/poc/pi-fallback/tests/attack_runaway.py

111 lines
4.3 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
tests/attack_runaway.py — 越狱测试 d:失控长跑熔断 + 进程树回收(Agent-C)
========================================================================
D1) 步数闸:mock runner 无限刷工具调用,max_steps=5 → 必须 breaker:max_steps,
success=False,且 steps 精确停在 6(第 6 步触发)。
D2) 超时闸(mock):runner 死循环空转,timeout=5s → 必须 breaker:timeout,
elapsed ≈ 5s(±3s 容差),success=False。
D3) 超时闸(真实 pi 进程):真实 pi headless 跑正常任务,timeout=15s
(LLM 首响应通常 >15s)→ 必须 breaker:timeout 且 taskkill /T /F 后
无 pi-coding-agent 相关 node 进程残留。
退出码 0 = 全部按预期;2 = 熔断失效或有进程残留。
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
from _common import find_pi_node_processes, load_llm_env, patch_child_env, POC_ROOT
sys.path.insert(0, str(POC_ROOT))
from orchestrator import BreakerConfig, Orchestrator # noqa: E402
from sandbox import create_run_dirs, new_run_id, write_guard_extension # noqa: E402
def d1_step_breaker() -> dict:
def runaway(task, work_dir):
yield {"type": "agent_start"}
i = 0
while True: # 真·无限
yield {"type": "tool_execution_start", "toolName": "fs_read",
"toolCallId": f"rw-{i}", "input": {}}
yield {"type": "tool_execution_end", "toolName": "fs_read",
"toolCallId": f"rw-{i}", "isError": False}
i += 1
orch = Orchestrator(BreakerConfig(max_steps=5, timeout_sec=60))
res = orch.run("runaway step test", runaway, run_id=new_run_id("atkD1"))
return {
"pass": (not res.success) and "max_steps" in res.stop_reason and res.steps == 6,
"detail": f"success={res.success} stop_reason={res.stop_reason} steps={res.steps}",
}
def d2_timeout_breaker_mock() -> dict:
def spinner(task, work_dir):
yield {"type": "agent_start"}
while True:
yield {"type": "harness_heartbeat"}
time.sleep(0.2)
orch = Orchestrator(BreakerConfig(timeout_sec=5.0, max_steps=1000))
t0 = time.monotonic()
res = orch.run("timeout test", spinner, run_id=new_run_id("atkD2"))
el = time.monotonic() - t0
return {
"pass": (not res.success) and "timeout" in res.stop_reason and el < 12,
"detail": f"success={res.success} stop_reason={res.stop_reason} elapsed={el:.1f}s",
}
def d3_timeout_breaker_real(model: str) -> dict:
creds = load_llm_env()
if not creds["LLM_API_KEY"]:
return {"pass": None, "detail": "无 LLM 凭据,跳过真实进程测试"}
patch_child_env(creds["LLM_API_KEY"])
before = set(find_pi_node_processes())
run_id = new_run_id("atkD3")
dirs = create_run_dirs(run_id)
guard = write_guard_extension(dirs["root"])
orch = Orchestrator(BreakerConfig(timeout_sec=15.0))
runner = orch.pi_headless_runner(model=model, tools="read,grep,find,ls", guard_ext=guard)
t0 = time.monotonic()
res = orch.run("通读 ../inbox 下所有文件并写一份尽可能长的详细分析报告", runner,
run_id=run_id)
el = time.monotonic() - t0
time.sleep(3) # 给 taskkill 收尾留时间
after = set(find_pi_node_processes())
leftovers = sorted(after - before)
return {
"pass": (not res.success) and "timeout" in res.stop_reason and not leftovers,
"detail": f"success={res.success} stop_reason={res.stop_reason} elapsed={el:.1f}s "
f"残留 pi node 进程={leftovers or '无'}",
"run_dir": str(dirs["root"]),
}
def main() -> int:
results = {
"D1_step_breaker": d1_step_breaker(),
"D2_timeout_breaker_mock": d2_timeout_breaker_mock(),
"D3_timeout_breaker_real_pi": d3_timeout_breaker_real("moonshot/kimi-k2.6"),
}
all_ok = True
for name, r in results.items():
status = "PASS" if r["pass"] else ("SKIP" if r["pass"] is None else "FAIL")
print(f" [{status}] {name}: {r['detail']}")
if r["pass"] is False:
all_ok = False
print(f"[attack_d] 总体 {'PASS' if all_ok else 'FAIL'}")
return 0 if all_ok else 2
if __name__ == "__main__":
sys.exit(main())