M5:百工与司命——能力注册表、授契行契请命、禁域裁决与观察标证
- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明) - 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301 - 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账 - MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch) - 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证) - 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页 - 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
This commit is contained in:
1 parent
144415247e
commit
684261f4b4
97 files changed
+3075
-5554
No files matched your search
@@ -1,50 +0,0 @@
|
||||
"""测试目标上下文注入。"""
|
||||
|
||||
from app.modules.runtime.goals import build_goals_context
|
||||
|
||||
|
||||
def test_build_goals_context_with_active_goals():
|
||||
"""生成了 Active Goals 系统提示片段。"""
|
||||
goals = [
|
||||
{
|
||||
"title": "学习 Python",
|
||||
"level": "long_term",
|
||||
"status": "active",
|
||||
"priority": 80,
|
||||
"progress": 0.5,
|
||||
},
|
||||
{
|
||||
"title": "完成项目文档",
|
||||
"level": "task",
|
||||
"status": "active",
|
||||
"priority": 60,
|
||||
"progress": 0.0,
|
||||
"parent_id": "goal_1",
|
||||
},
|
||||
]
|
||||
result = build_goals_context(goals)
|
||||
assert "学习 Python" in result
|
||||
assert "任务目标" in result or "task" in result or "长期目标" in result
|
||||
assert result.startswith("当前活跃目标:") or "当前" in result
|
||||
|
||||
|
||||
def test_build_goals_context_empty():
|
||||
assert build_goals_context([]) == ""
|
||||
|
||||
|
||||
def test_build_goals_context_sorts_by_priority():
|
||||
"""按优先级降序排列输出。"""
|
||||
goals = [
|
||||
{"title": "low", "level": "task", "status": "active", "priority": 10, "progress": 0.0},
|
||||
{
|
||||
"title": "high",
|
||||
"level": "long_term",
|
||||
"status": "active",
|
||||
"priority": 90,
|
||||
"progress": 0.5,
|
||||
},
|
||||
]
|
||||
result = build_goals_context(goals)
|
||||
high_idx = result.index("high")
|
||||
low_idx = result.index("low")
|
||||
assert high_idx < low_idx # high priority first
|
||||
@@ -1,120 +0,0 @@
|
||||
"""DbConfirmationStore 单元测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.db.base import Base
|
||||
from app.modules.safety.confirmation import (
|
||||
DbConfirmationStore,
|
||||
PendingConfirmation,
|
||||
new_confirmation_id,
|
||||
)
|
||||
|
||||
_TEST_DATABASE_URL = "sqlite+aiosqlite://"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
engine = create_async_engine(_TEST_DATABASE_URL, echo=False)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def store(db_session: AsyncSession) -> DbConfirmationStore:
|
||||
"""创建绑定 db_session 的 DbConfirmationStore。"""
|
||||
session_factory = sessionmaker(db_session.bind, class_=AsyncSession, expire_on_commit=False)
|
||||
return DbConfirmationStore(session_factory)
|
||||
|
||||
|
||||
class TestDbConfirmationStore:
|
||||
"""数据库确认存储 CRUD 测试。"""
|
||||
|
||||
async def test_create_and_get(self, store: DbConfirmationStore) -> None:
|
||||
pending = PendingConfirmation(
|
||||
confirmation_id=new_confirmation_id(),
|
||||
tool="fetch_url",
|
||||
input={"q": "test"},
|
||||
risk_level="L2",
|
||||
reason="测试确认",
|
||||
)
|
||||
created = await store.create(pending)
|
||||
assert created.confirmation_id == pending.confirmation_id
|
||||
|
||||
fetched = await store.get(pending.confirmation_id)
|
||||
assert fetched is not None
|
||||
assert fetched.tool == "fetch_url"
|
||||
assert fetched.input == {"q": "test"}
|
||||
|
||||
async def test_get_non_existent(self, store: DbConfirmationStore) -> None:
|
||||
result = await store.get("confirm_nonexistent")
|
||||
assert result is None
|
||||
|
||||
async def test_pop_removes_and_returns(self, store: DbConfirmationStore) -> None:
|
||||
pending = PendingConfirmation(
|
||||
confirmation_id=new_confirmation_id(),
|
||||
tool="shell",
|
||||
input={"cmd": "ls"},
|
||||
risk_level="L3",
|
||||
reason="高风险操作",
|
||||
)
|
||||
await store.create(pending)
|
||||
|
||||
popped = await store.pop(pending.confirmation_id)
|
||||
assert popped is not None
|
||||
assert popped.tool == "shell"
|
||||
|
||||
# pop 后不应再可取到
|
||||
fetched = await store.get(pending.confirmation_id)
|
||||
assert fetched is None
|
||||
|
||||
async def test_pop_non_existent(self, store: DbConfirmationStore) -> None:
|
||||
result = await store.pop("confirm_nonexistent")
|
||||
assert result is None
|
||||
|
||||
async def test_list_pending(self, store: DbConfirmationStore) -> None:
|
||||
# 插入两条
|
||||
id1 = new_confirmation_id()
|
||||
id2 = new_confirmation_id()
|
||||
await store.create(
|
||||
PendingConfirmation(
|
||||
confirmation_id=id1, tool="fetch_url", input={}, risk_level="L1", reason="a"
|
||||
)
|
||||
)
|
||||
await store.create(
|
||||
PendingConfirmation(
|
||||
confirmation_id=id2, tool="shell", input={}, risk_level="L3", reason="b"
|
||||
)
|
||||
)
|
||||
items = await store.list_pending()
|
||||
assert len(items) == 2
|
||||
# 按创建时间升序
|
||||
assert items[0].confirmation_id == id1
|
||||
assert items[1].confirmation_id == id2
|
||||
|
||||
async def test_list_pending_empty(self, store: DbConfirmationStore) -> None:
|
||||
items = await store.list_pending()
|
||||
assert items == []
|
||||
|
||||
async def test_popped_not_in_list_pending(self, store: DbConfirmationStore) -> None:
|
||||
pending = PendingConfirmation(
|
||||
confirmation_id=new_confirmation_id(),
|
||||
tool="fetch_url",
|
||||
input={},
|
||||
risk_level="L2",
|
||||
reason="测试",
|
||||
)
|
||||
await store.create(pending)
|
||||
await store.pop(pending.confirmation_id)
|
||||
items = await store.list_pending()
|
||||
ids = [i.confirmation_id for i in items]
|
||||
assert pending.confirmation_id not in ids
|
||||
@@ -1,66 +0,0 @@
|
||||
"""司命决策器测试:风险等级 × 权限策略 × 是否确认 的决策矩阵,以及策略字典加载。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.safety.governor import (
|
||||
PermissionPolicy,
|
||||
SafetyDecision,
|
||||
SafetyGovernor,
|
||||
ToolPolicy,
|
||||
)
|
||||
|
||||
|
||||
def test_low_risk_defaults_to_allow() -> None:
|
||||
governor = SafetyGovernor()
|
||||
result = governor.evaluate(tool="calc", risk_level="L1")
|
||||
assert result.decision is SafetyDecision.allow
|
||||
|
||||
|
||||
def test_medium_risk_unconfirmed_asks() -> None:
|
||||
governor = SafetyGovernor()
|
||||
result = governor.evaluate(tool="x", risk_level="L2")
|
||||
assert result.decision is SafetyDecision.ask
|
||||
|
||||
|
||||
def test_confirmed_becomes_allow() -> None:
|
||||
governor = SafetyGovernor()
|
||||
result = governor.evaluate(tool="x", risk_level="L3", confirmed=True)
|
||||
assert result.decision is SafetyDecision.allow
|
||||
|
||||
|
||||
def test_l4_always_denied() -> None:
|
||||
governor = SafetyGovernor()
|
||||
result = governor.evaluate(tool="x", risk_level="L4")
|
||||
assert result.decision is SafetyDecision.deny
|
||||
|
||||
|
||||
def test_forbidden_policy_denied() -> None:
|
||||
governor = SafetyGovernor(
|
||||
{"pay": ToolPolicy(risk_level="L3", permission_policy=PermissionPolicy.forbidden)}
|
||||
)
|
||||
result = governor.evaluate(tool="pay", risk_level="L3")
|
||||
assert result.decision is SafetyDecision.deny
|
||||
assert result.policy is PermissionPolicy.forbidden
|
||||
|
||||
|
||||
def test_explicit_tool_policy_overrides_default() -> None:
|
||||
governor = SafetyGovernor(
|
||||
{"read": ToolPolicy(risk_level="L2", permission_policy=PermissionPolicy.allow)}
|
||||
)
|
||||
result = governor.evaluate(tool="read")
|
||||
assert result.decision is SafetyDecision.allow
|
||||
|
||||
|
||||
def test_from_policy_dict_loads_policies() -> None:
|
||||
governor = SafetyGovernor.from_policy_dict(
|
||||
{"tools": {"custom": {"risk_level": "L2", "permission_policy": "allow"}}}
|
||||
)
|
||||
result = governor.evaluate(tool="custom")
|
||||
assert result.decision is SafetyDecision.allow
|
||||
|
||||
|
||||
def test_from_policy_dict_empty_returns_default() -> None:
|
||||
governor = SafetyGovernor.from_policy_dict({})
|
||||
# 空 dict → 空 Governor,未列工具按 risk 推导:L2 → ask
|
||||
result = governor.evaluate(tool="x", risk_level="L2")
|
||||
assert result.decision is SafetyDecision.ask
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Skill 关键词触发匹配纯函数测试。"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.modules.skills.matcher import match_skills
|
||||
|
||||
|
||||
def _skill(name: str, trigger: str, status: str = "enabled") -> SimpleNamespace:
|
||||
return SimpleNamespace(name=name, trigger=trigger, status=status)
|
||||
|
||||
|
||||
def test_match_by_keyword() -> None:
|
||||
skills = [_skill("coding", "代码,编程")]
|
||||
assert len(match_skills("帮我写代码", skills)) == 1
|
||||
|
||||
|
||||
def test_no_match() -> None:
|
||||
skills = [_skill("coding", "代码,编程")]
|
||||
assert match_skills("今天天气不错", skills) == []
|
||||
|
||||
|
||||
def test_case_insensitive() -> None:
|
||||
skills = [_skill("en", "Python")]
|
||||
assert len(match_skills("用 python 写", skills)) == 1
|
||||
|
||||
|
||||
def test_multiple_keywords_any_match() -> None:
|
||||
skills = [_skill("dev", "代码,debug,编程")]
|
||||
assert len(match_skills("帮我 debug", skills)) == 1
|
||||
|
||||
|
||||
def test_disabled_not_matched() -> None:
|
||||
skills = [_skill("coding", "代码", status="disabled")]
|
||||
assert match_skills("写代码", skills) == []
|
||||
|
||||
|
||||
def test_empty_trigger_not_matched() -> None:
|
||||
skills = [_skill("empty", "")]
|
||||
assert match_skills("任意文本", skills) == []
|
||||
|
||||
|
||||
def test_multiple_skills_order_preserved() -> None:
|
||||
skills = [_skill("a", "代码"), _skill("b", "天气"), _skill("c", "编程")]
|
||||
matched = match_skills("写代码和编程", skills)
|
||||
assert [s.name for s in matched] == ["a", "c"]
|
||||
|
||||
|
||||
def test_empty_context() -> None:
|
||||
skills = [_skill("a", "代码")]
|
||||
assert match_skills("", skills) == []
|
||||
|
||||
|
||||
def test_whitespace_only_trigger_ignored() -> None:
|
||||
skills = [_skill("ws", " , , ")]
|
||||
assert match_skills("任意文本", skills) == []
|
||||
Reference in new issue
Block a user