Files
xiaoli-cli/tests/test_permissions.py
T
shuiyu67andWorkBuddy 160d4e924e feat: 声明式权限(opencode 式 allow/ask/deny 配置化)
- safety.check 在 plan 模式之后、安全模式之前插入声明式权限判定:
  deny(阻断) > ask(强制确认) > allow(自动放行) > 默认(按安全模式)
- 规则语法支持 "*"全匹配 / "tool"精确 / "tool:*"任意操作 / "tool:op"操作前缀
- 读取 config.permissions;提供 set_permissions 显式注入(测试/启动用)
- config.json 顶层加 permissions 段(allow/ask/deny 示例,该文件被 gitignore 不入库)
- 修复:test_normal_mode_does_not_plan_block 隔离真实 config 权限,避免污染
- 新增 tests/test_permissions.py(14 项:规则匹配+deny/ask/allow+优先级)

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
2026-08-05 17:56:25 +08:00

117 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""声明式权限(allow/ask/deny)回归测试。不触真实引擎/网络。"""
import pytest
from xcli_core.safety import SafetyLayer, MODE_UNRESTRICTED, MODE_NORMAL, MODE_MANUAL
def _new_safety(allow=None, ask=None, deny=None, mode=MODE_NORMAL):
s = SafetyLayer()
s.set_permissions(allow=allow, ask=ask, deny=deny)
s.mode = mode
return s
# ── 纯函数:规则匹配 ──
def test_match_wildcard():
s = SafetyLayer()
assert s._permission_match("*", "anything", "rm -rf /") is True
def test_match_exact_tool():
s = SafetyLayer()
assert s._permission_match("git_tools", "git_tools", "status") is True
assert s._permission_match("git_tools", "cmd_executor", "status") is False
def test_match_tool_star():
s = SafetyLayer()
assert s._permission_match("git_tools:*", "git_tools", "push origin") is True
def test_match_tool_op_prefix():
s = SafetyLayer()
assert s._permission_match("cmd_executor:rm", "cmd_executor", "rm -rf /tmp") is True
assert s._permission_match("cmd_executor:rm", "cmd_executor", "ls -la") is False
# 操作词精确相等也应命中
assert s._permission_match("git_tools:push", "git_tools", "push") is True
def test_match_empty_rule_ignored():
s = SafetyLayer()
assert s._permission_match("", "git_tools", "status") is False
# ── deny 最高优先级 ──
def test_deny_blocks():
s = _new_safety(deny=["git_tools:reset"], mode=MODE_UNRESTRICTED)
ok, msg = s.check("git_tools", "reset --hard HEAD")
assert ok is False
assert "deny" in msg
def test_deny_blocks_even_unrestricted():
s = _new_safety(deny=["cmd_executor:rm"], mode=MODE_UNRESTRICTED)
ok, _ = s.check("cmd_executor", "rm -rf /")
assert ok is False
def test_deny_overrides_allow():
# 同一工具同时出现在 deny 与 allow,deny 胜出
s = _new_safety(deny=["git_tools:reset"], allow=["git_tools:reset"], mode=MODE_NORMAL)
ok, _ = s.check("git_tools", "reset --hard")
assert ok is False
# ── ask 强制确认 ──
def test_ask_forces_confirm():
s = _new_safety(ask=["cmd_executor"], mode=MODE_NORMAL)
confirmed = []
s._ask_user_confirm = lambda *a, **k: (confirmed.append(True) or (True, "ok"))
ok, _ = s.check("cmd_executor", "ls")
assert ok is True
assert confirmed == [True]
def test_ask_overrides_allow():
# 同时 allow 与 ask,ask(确认)胜出,不会自动放行
s = _new_safety(allow=["cmd_executor"], ask=["cmd_executor"], mode=MODE_NORMAL)
called = []
s._ask_user_confirm = lambda *a, **k: (called.append(True) or (True, "ok"))
s.check("cmd_executor", "ls")
assert called == [True]
def test_ask_works_in_unrestricted():
s = _new_safety(ask=["git_tools:push"], mode=MODE_UNRESTRICTED)
called = []
s._ask_user_confirm = lambda *a, **k: (called.append(True) or (False, "no"))
ok, _ = s.check("git_tools", "push origin master")
assert ok is False
assert called == [True]
# ── allow 自动放行 ──
def test_allow_auto_passes():
s = _new_safety(allow=["git_tools:status"], mode=MODE_NORMAL)
# 不应触发 AI 分析 / 确认
s._ask_user_confirm = lambda *a, **k: (False, "SHOULD_NOT_BE_CALLED")
ok, msg = s.check("git_tools", "status")
assert ok is True
assert msg == ""
def test_allow_no_match_falls_through():
s = _new_safety(allow=["git_tools:status"], mode=MODE_UNRESTRICTED)
ok, _ = s.check("cmd_executor", "ls")
# 无规则命中 → 走默认(无限制模式直接放行)
assert ok is True
def test_default_without_rules_unrestricted_passes():
s = _new_safety(mode=MODE_UNRESTRICTED)
ok, _ = s.check("file_manager", "read foo.txt")
assert ok is True