- 百工域 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 个预存环境失败不变)
527 lines
18 KiB
Python
527 lines
18 KiB
Python
"""司命台治理对象 CRUD 服务(api-spec §19.4)。
|
||
|
||
五组开放集合(目标 / 感知通道 / 世界实体 / 计划 / 天工阁角色)的增删改查;
|
||
治理写操作一律记审计事件(audit_),并入起居注(§19.3)。pivot 由 action 前缀推断。
|
||
|
||
所有函数自管 ``AsyncSessionLocal``(路由直接调用,不传入 session)。
|
||
"""
|
||
|
||
from app.core.errors import AppError
|
||
from app.core.time import utc_now
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models.governance import (
|
||
AgentRosterEntry,
|
||
Goal,
|
||
PerceptionChannel,
|
||
Plan,
|
||
WorldEntity,
|
||
)
|
||
from app.modules.runtime.events import Event
|
||
from app.repositories import governance as governance_repo
|
||
from app.repositories import runtime as runtime_repo
|
||
from app.repositories.json_utils import dumps, loads_dict
|
||
from app.schemas.admin import (
|
||
AgentRosterResponse,
|
||
CreateAgentRequest,
|
||
CreateGoalRequest,
|
||
CreatePerceptionChannelRequest,
|
||
CreatePlanRequest,
|
||
CreateWorldEntityRequest,
|
||
GoalNode,
|
||
PerceptionChannelResponse,
|
||
PlanSummaryResponse,
|
||
UpdateAgentRequest,
|
||
UpdateGoalRequest,
|
||
UpdatePerceptionChannelRequest,
|
||
UpdatePlanRequest,
|
||
UpdateWorldEntityRequest,
|
||
WorldEntityResponse,
|
||
)
|
||
from app.schemas.safety import (
|
||
ToolCallDetail,
|
||
)
|
||
from app.services.audit import record_audit_event
|
||
from app.services.serializers import (
|
||
agent_to_response,
|
||
goal_to_response,
|
||
perception_channel_to_response,
|
||
plan_to_response,
|
||
world_entity_to_response,
|
||
)
|
||
|
||
# —— 太一枢 · 目标 ——
|
||
|
||
|
||
async def list_goals() -> list[GoalNode]:
|
||
async with AsyncSessionLocal() as session:
|
||
goals = await governance_repo.list_goals(session)
|
||
return [goal_to_response(goal) for goal in goals]
|
||
|
||
|
||
async def list_goals_by_character(
|
||
character_id: str,
|
||
status_filter: str | None = None,
|
||
) -> list[GoalNode]:
|
||
async with AsyncSessionLocal() as session:
|
||
goals = await governance_repo.list_goals_by_character(
|
||
session, character_id, status_filter=status_filter
|
||
)
|
||
return [goal_to_response(goal) for goal in goals]
|
||
|
||
|
||
async def load_active_goals_context(character_id: str) -> str | None:
|
||
"""加载活跃目标并格式化为 system prompt 片段,供运行时注入。
|
||
|
||
目标按优先级排序(``prioritize_active_goals``),最高优先级在前。
|
||
"""
|
||
async with AsyncSessionLocal() as session:
|
||
goals = await governance_repo.list_goals_by_character(
|
||
session, character_id, status_filter="active"
|
||
)
|
||
if not goals:
|
||
return None
|
||
lines = [
|
||
f"- [{g.level}] {g.title}(优先级 {g.priority},进度 {g.progress}%)"
|
||
for g in sorted(goals, key=lambda g: -(g.priority or 0))
|
||
]
|
||
return "【当前目标】\n" + "\n".join(lines)
|
||
|
||
|
||
async def emit_goal_event(
|
||
goal: Goal,
|
||
event_type: str,
|
||
extra_payload: dict | None = None,
|
||
) -> None:
|
||
"""向事件存储写入 goal.* 事件。"""
|
||
from app.main import app # 惰性 import 避免启动循环
|
||
|
||
event_store = getattr(app.state, "event_store", None)
|
||
if event_store is None:
|
||
return
|
||
|
||
payload = {
|
||
"goal_id": goal.id,
|
||
"character_id": goal.character_id,
|
||
"title": goal.title,
|
||
"level": goal.level,
|
||
"type": goal.type,
|
||
"status": goal.status,
|
||
"priority": goal.priority,
|
||
"progress": goal.progress,
|
||
}
|
||
if extra_payload:
|
||
payload.update(extra_payload)
|
||
|
||
event = Event(
|
||
type=f"goal.{event_type}",
|
||
source="goal_orchestrator",
|
||
payload=payload,
|
||
character_id=goal.character_id,
|
||
visibility="audit",
|
||
)
|
||
await event_store.append(event)
|
||
|
||
|
||
async def create_goal(payload: CreateGoalRequest) -> GoalNode:
|
||
async with AsyncSessionLocal() as session:
|
||
goal = Goal(
|
||
title=payload.title,
|
||
description=payload.description,
|
||
level=payload.level,
|
||
type=payload.type,
|
||
character_id=payload.character_id,
|
||
status="pending",
|
||
priority=payload.priority,
|
||
constraints_json=dumps(payload.constraints),
|
||
parent_id=payload.parent_id,
|
||
success_criteria_json=dumps(payload.success_criteria),
|
||
)
|
||
await governance_repo.add_goal(session, goal)
|
||
await record_audit_event(
|
||
session,
|
||
action="goal.created",
|
||
actor="user",
|
||
input_summary=goal.title,
|
||
resource_type="goal",
|
||
resource_id=goal.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(goal)
|
||
await emit_goal_event(goal, "created")
|
||
return goal_to_response(goal)
|
||
|
||
|
||
async def update_goal(goal_id: str, payload: UpdateGoalRequest) -> GoalNode:
|
||
async with AsyncSessionLocal() as session:
|
||
goal = await governance_repo.get_goal(session, goal_id)
|
||
if goal is None:
|
||
raise AppError("not_found", "目标不存在。")
|
||
old_status = goal.status
|
||
updates = payload.model_dump(exclude_unset=True)
|
||
constraints = updates.pop("constraints", None)
|
||
for key, value in updates.items():
|
||
setattr(goal, key, value)
|
||
if constraints is not None:
|
||
goal.constraints_json = dumps(constraints)
|
||
goal.updated_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="goal.updated",
|
||
actor="user",
|
||
input_summary=goal.title,
|
||
resource_type="goal",
|
||
resource_id=goal.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(goal)
|
||
if old_status != goal.status:
|
||
if goal.status == "completed":
|
||
await emit_goal_event(goal, "completed", {"previous_status": old_status})
|
||
else:
|
||
await emit_goal_event(goal, "updated", {"previous_status": old_status})
|
||
else:
|
||
await emit_goal_event(goal, "updated", {"previous_status": old_status})
|
||
return goal_to_response(goal)
|
||
|
||
|
||
async def delete_goal(goal_id: str) -> None:
|
||
async with AsyncSessionLocal() as session:
|
||
goal = await governance_repo.get_goal(session, goal_id)
|
||
if goal is None:
|
||
raise AppError("not_found", "目标不存在。")
|
||
if goal.level == "highest":
|
||
raise AppError("invalid_request", "最高目标(使命级)不可删除。")
|
||
await session.delete(goal)
|
||
await record_audit_event(
|
||
session,
|
||
action="goal.deleted",
|
||
actor="user",
|
||
input_summary=goal.title,
|
||
resource_type="goal",
|
||
resource_id=goal.id,
|
||
)
|
||
await session.commit()
|
||
|
||
|
||
# —— 烛照枢 · 感知通道 ——
|
||
|
||
|
||
async def list_perception_channels() -> list[PerceptionChannelResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
channels = await governance_repo.list_perception_channels(session)
|
||
return [perception_channel_to_response(channel) for channel in channels]
|
||
|
||
|
||
async def create_perception_channel(
|
||
payload: CreatePerceptionChannelRequest,
|
||
) -> PerceptionChannelResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
if await governance_repo.get_perception_channel(session, payload.channel) is not None:
|
||
raise AppError("conflict", "感知通道已存在。")
|
||
channel = PerceptionChannel(
|
||
channel=payload.channel,
|
||
name=payload.name,
|
||
description=payload.description,
|
||
risk_level=payload.risk_level,
|
||
)
|
||
await governance_repo.add_perception_channel(session, channel)
|
||
await record_audit_event(
|
||
session,
|
||
action="perception.created",
|
||
actor="user",
|
||
input_summary=channel.name,
|
||
resource_type="perception_channel",
|
||
resource_id=channel.channel,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(channel)
|
||
return perception_channel_to_response(channel)
|
||
|
||
|
||
async def update_perception_channel(
|
||
channel_id: str, payload: UpdatePerceptionChannelRequest
|
||
) -> PerceptionChannelResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
channel = await governance_repo.get_perception_channel(session, channel_id)
|
||
if channel is None:
|
||
raise AppError("not_found", "感知通道不存在。")
|
||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||
setattr(channel, key, value)
|
||
channel.updated_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="perception.updated",
|
||
actor="user",
|
||
input_summary=channel.name,
|
||
resource_type="perception_channel",
|
||
resource_id=channel.channel,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(channel)
|
||
return perception_channel_to_response(channel)
|
||
|
||
|
||
async def delete_perception_channel(channel_id: str) -> None:
|
||
async with AsyncSessionLocal() as session:
|
||
channel = await governance_repo.get_perception_channel(session, channel_id)
|
||
if channel is None:
|
||
raise AppError("not_found", "感知通道不存在。")
|
||
await session.delete(channel)
|
||
await record_audit_event(
|
||
session,
|
||
action="perception.deleted",
|
||
actor="user",
|
||
input_summary=channel.name,
|
||
resource_type="perception_channel",
|
||
resource_id=channel.channel,
|
||
)
|
||
await session.commit()
|
||
|
||
|
||
# —— 河图枢 · 世界实体 ——
|
||
|
||
|
||
async def list_world_entities() -> list[WorldEntityResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
entities = await governance_repo.list_world_entities(session)
|
||
return [world_entity_to_response(entity) for entity in entities]
|
||
|
||
|
||
async def create_world_entity(
|
||
payload: CreateWorldEntityRequest,
|
||
) -> WorldEntityResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
entity = WorldEntity(
|
||
type=payload.type,
|
||
name=payload.name,
|
||
attributes_json=dumps(payload.attributes),
|
||
state_type=payload.state_type,
|
||
confidence=payload.confidence,
|
||
sources_json=dumps(payload.sources),
|
||
)
|
||
await governance_repo.add_world_entity(session, entity)
|
||
await record_audit_event(
|
||
session,
|
||
action="world.created",
|
||
actor="user",
|
||
input_summary=entity.name,
|
||
resource_type="world_entity",
|
||
resource_id=entity.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(entity)
|
||
return world_entity_to_response(entity)
|
||
|
||
|
||
async def update_world_entity(
|
||
entity_id: str, payload: UpdateWorldEntityRequest
|
||
) -> WorldEntityResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
entity = await governance_repo.get_world_entity(session, entity_id)
|
||
if entity is None:
|
||
raise AppError("not_found", "世界实体不存在。")
|
||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||
setattr(entity, key, value)
|
||
entity.updated_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="world.updated",
|
||
actor="user",
|
||
input_summary=entity.name,
|
||
resource_type="world_entity",
|
||
resource_id=entity.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(entity)
|
||
return world_entity_to_response(entity)
|
||
|
||
|
||
async def delete_world_entity(entity_id: str) -> None:
|
||
async with AsyncSessionLocal() as session:
|
||
entity = await governance_repo.get_world_entity(session, entity_id)
|
||
if entity is None:
|
||
raise AppError("not_found", "世界实体不存在。")
|
||
await session.delete(entity)
|
||
await record_audit_event(
|
||
session,
|
||
action="world.deleted",
|
||
actor="user",
|
||
input_summary=entity.name,
|
||
resource_type="world_entity",
|
||
resource_id=entity.id,
|
||
)
|
||
await session.commit()
|
||
|
||
|
||
# —— 洛书枢 · 计划 ——
|
||
|
||
|
||
async def list_plans() -> list[PlanSummaryResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
plans = await governance_repo.list_plans(session)
|
||
return [plan_to_response(plan) for plan in plans]
|
||
|
||
|
||
async def create_plan(payload: CreatePlanRequest) -> PlanSummaryResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
plan = Plan(
|
||
goal_id=payload.goal_id,
|
||
title=payload.title,
|
||
assumptions_json=dumps(payload.assumptions),
|
||
risks_json=dumps(payload.risks),
|
||
)
|
||
await governance_repo.add_plan(session, plan)
|
||
await record_audit_event(
|
||
session,
|
||
action="plan.created",
|
||
actor="user",
|
||
input_summary=plan.title,
|
||
resource_type="plan",
|
||
resource_id=plan.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(plan)
|
||
return plan_to_response(plan)
|
||
|
||
|
||
async def update_plan(plan_id: str, payload: UpdatePlanRequest) -> PlanSummaryResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
plan = await governance_repo.get_plan(session, plan_id)
|
||
if plan is None:
|
||
raise AppError("not_found", "计划不存在。")
|
||
updates = payload.model_dump(exclude_unset=True)
|
||
assumptions = updates.pop("assumptions", None)
|
||
risks = updates.pop("risks", None)
|
||
for key, value in updates.items():
|
||
setattr(plan, key, value)
|
||
if assumptions is not None:
|
||
plan.assumptions_json = dumps(assumptions)
|
||
if risks is not None:
|
||
plan.risks_json = dumps(risks)
|
||
plan.updated_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="plan.updated",
|
||
actor="user",
|
||
input_summary=plan.title,
|
||
resource_type="plan",
|
||
resource_id=plan.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(plan)
|
||
return plan_to_response(plan)
|
||
|
||
|
||
async def delete_plan(plan_id: str) -> None:
|
||
async with AsyncSessionLocal() as session:
|
||
plan = await governance_repo.get_plan(session, plan_id)
|
||
if plan is None:
|
||
raise AppError("not_found", "计划不存在。")
|
||
await session.delete(plan)
|
||
await record_audit_event(
|
||
session,
|
||
action="plan.deleted",
|
||
actor="user",
|
||
input_summary=plan.title,
|
||
resource_type="plan",
|
||
resource_id=plan.id,
|
||
)
|
||
await session.commit()
|
||
|
||
|
||
# —— 天工阁枢 · 角色在岗 ——
|
||
|
||
|
||
async def list_agent_roster() -> list[AgentRosterResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
agents = await governance_repo.list_agent_roster(session)
|
||
return [agent_to_response(agent) for agent in agents]
|
||
|
||
|
||
async def create_agent(payload: CreateAgentRequest) -> AgentRosterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
if await governance_repo.get_agent(session, payload.role) is not None:
|
||
raise AppError("conflict", "角色已存在。")
|
||
agent = AgentRosterEntry(
|
||
role=payload.role,
|
||
name=payload.name,
|
||
responsibility=payload.responsibility,
|
||
)
|
||
await governance_repo.add_agent(session, agent)
|
||
await record_audit_event(
|
||
session,
|
||
action="agent.created",
|
||
actor="user",
|
||
input_summary=agent.name,
|
||
resource_type="agent",
|
||
resource_id=agent.role,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(agent)
|
||
return agent_to_response(agent)
|
||
|
||
|
||
async def update_agent(role: str, payload: UpdateAgentRequest) -> AgentRosterResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
agent = await governance_repo.get_agent(session, role)
|
||
if agent is None:
|
||
raise AppError("not_found", "角色不存在。")
|
||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||
setattr(agent, key, value)
|
||
agent.updated_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="agent.updated",
|
||
actor="user",
|
||
input_summary=agent.name,
|
||
resource_type="agent",
|
||
resource_id=agent.role,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(agent)
|
||
return agent_to_response(agent)
|
||
|
||
|
||
async def delete_agent(role: str) -> None:
|
||
async with AsyncSessionLocal() as session:
|
||
agent = await governance_repo.get_agent(session, role)
|
||
if agent is None:
|
||
raise AppError("not_found", "角色不存在。")
|
||
await session.delete(agent)
|
||
await record_audit_event(
|
||
session,
|
||
action="agent.deleted",
|
||
actor="user",
|
||
input_summary=agent.name,
|
||
resource_type="agent",
|
||
resource_id=agent.role,
|
||
)
|
||
await session.commit()
|
||
|
||
|
||
# —— 百工枢 · 工具策略(§15 / §16 共享)——
|
||
|
||
|
||
async def get_tool_call(tool_call_id: str) -> ToolCallDetail:
|
||
"""GET /tools/calls/{tool_call_id}:单条 tool.* 事件详情。
|
||
|
||
tool_call_id 对应一炁事件存储的事件主键;仅返回 tool.* 类型事件,否则 404。
|
||
"""
|
||
from app.core.time import to_iso
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
event = await runtime_repo.get_event(session, tool_call_id)
|
||
if event is None or not event.type.startswith("tool."):
|
||
raise AppError("not_found", f"工具调用 {tool_call_id} 不存在。")
|
||
payload = loads_dict(event.payload_json)
|
||
return ToolCallDetail(
|
||
event_id=event.id,
|
||
type=event.type,
|
||
tool=payload.get("tool"),
|
||
run_id=event.run_id,
|
||
character_id=event.character_id,
|
||
trace_id=event.trace_id,
|
||
payload=payload,
|
||
created_at=to_iso(event.created_at),
|
||
)
|