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:
chenxin committed 2026-08-27 19:17:21 +08:00
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) == []