639 lines
31 KiB
Python
639 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
pbl_agent_runtime M4a — Agent 定义与工具注册表(含 seed 幂等注入)
|
||
|
||
内容:
|
||
1) AGENT_DEFS:第 13.1 章仅 designer / critic 两个 Agent;critic write_allowed=0(零写权限)
|
||
2) TOOL_SEED:第 31 章裁剪子集 —— 13 enabled + 9 disabled(agent-tool-contract.md §2/§3)
|
||
3) register_tool / list_tools / set_tool_status / seed_tools / seed_agents(幂等,build.sh 调用)
|
||
|
||
四类强制人工审批(14.2):
|
||
publish(T12 publish.request)/ compile_execute(T11 compile.trigger)/
|
||
blueprint_approve(蓝图 approved 状态推进)/ tool_registry_change(注册表启停变更)
|
||
"""
|
||
|
||
from .m4a_kernel import (
|
||
get_store, one, fail, now_ts, dumps, loads, write_audit,
|
||
E_FORBIDDEN, E_DUPLICATE, E_NOT_FOUND,
|
||
TOOL_STATUS_ENABLED, TOOL_STATUS_DISABLED,
|
||
AGENT_STATUS_ENABLED, ACTOR_USER,
|
||
)
|
||
|
||
PERM_AUTHORING = "pbl_authoring"
|
||
PERM_AGENT_TOOLS = "agent_tools"
|
||
PERM_PUBLISHING = "publishing"
|
||
PERM_KDB = "kdb"
|
||
PERM_PLATFORM_ADMIN = "platform_admin"
|
||
|
||
# 四类强制人工审批
|
||
APPROVAL_TYPES = ("publish", "compile_execute", "blueprint_approve",
|
||
"tool_registry_change")
|
||
|
||
DESIGNER = "designer"
|
||
CRITIC = "critic"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. Agent 定义(第 13.1 章:仅此两个)
|
||
# ---------------------------------------------------------------------------
|
||
AGENT_DEFS = [
|
||
{
|
||
"agent_code": DESIGNER,
|
||
"agent_name": "Designer Agent",
|
||
"agent_type": "authoring",
|
||
"loop_pattern": "observe_think_propose_execute",
|
||
"authority_boundary":
|
||
"Agent 非真相源(14.1):Designer 只 Propose,全部写操作经工具裁决链落到"
|
||
"权威系统(pbl_blueprint / pbl_validation / pbl_compiler);"
|
||
"Execute 受裁决,Publish 必须人工审批(14.2)。",
|
||
"write_allowed": 1,
|
||
"allowed_tools": [
|
||
"blueprint.create", "blueprint.update", "blueprint.get",
|
||
"template.list", "template.copy",
|
||
"validation.run", "validation.report",
|
||
"trace.get", "approval.request",
|
||
"compile.trigger", "publish.request", "kdb.search",
|
||
],
|
||
# critic.review 属 critic 域,designer 不调用
|
||
"denied_tools": [
|
||
"critic.review",
|
||
"blueprint.publish_auto", "curriculum.modify_auto",
|
||
"marketplace.create_listing", "kdb.write", "research.collect",
|
||
"experiment.ab_run", "agent.mentor_invoke", "world.edit_3d",
|
||
"billing.charge",
|
||
],
|
||
"model_binding": {
|
||
"model": "", "capability": "t2t",
|
||
"fallback": "template_fallback",
|
||
"note": "模型不可达 → PBL_E_MODEL_UNAVAILABLE 内部捕获,自动切 template.copy 兜底(US-05)",
|
||
},
|
||
"offline_fallback": "template_fallback",
|
||
"status": AGENT_STATUS_ENABLED,
|
||
"description": "NL 意图 → Blueprint 草稿(生成 / 对话式修改提案)",
|
||
},
|
||
{
|
||
"agent_code": CRITIC,
|
||
"agent_name": "Critic Agent",
|
||
"agent_type": "review",
|
||
"loop_pattern": "observe_think_propose",
|
||
"authority_boundary":
|
||
"Critic 零写权限(14.1):不直接修改 Blueprint,仅产出建议"
|
||
"(recommendation / reason / evidence / confidence,14.3);"
|
||
"只读 blueprint.get + validation.report,无任何 write_operation=1 工具。",
|
||
"write_allowed": 0,
|
||
"allowed_tools": ["blueprint.get", "validation.report", "critic.review",
|
||
"trace.get"],
|
||
# 零写权限:所有写工具显式拉黑(S6 双重防线)
|
||
"denied_tools": [
|
||
"blueprint.create", "blueprint.update", "template.copy",
|
||
"validation.run", "compile.trigger", "publish.request",
|
||
"approval.request",
|
||
"blueprint.publish_auto", "curriculum.modify_auto",
|
||
"marketplace.create_listing", "kdb.write", "research.collect",
|
||
"experiment.ab_run", "agent.mentor_invoke", "world.edit_3d",
|
||
"billing.charge",
|
||
],
|
||
"model_binding": {
|
||
"model": "", "capability": "t2t", "fallback": "rule_based",
|
||
"note": "模型不可达 → 降级为规则式建议(读 validation.report 告警),仍产出四要素",
|
||
},
|
||
"offline_fallback": "none",
|
||
"status": AGENT_STATUS_ENABLED,
|
||
"description": "质量告警与改进建议(只读评审,不改蓝图)",
|
||
},
|
||
]
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 工具注册表 seed(13 enabled + 9 disabled)
|
||
# ---------------------------------------------------------------------------
|
||
def _t(code, name, group, status, perm, approval, agents, write, ischema,
|
||
oschema, backend, sort, disable_reason=None, approval_type=None,
|
||
scope_note=None):
|
||
return {
|
||
"tool_code": code, "tool_name": name, "tool_group": group, "status": status,
|
||
"required_permission": perm, "require_approval": approval,
|
||
"approval_action_type": approval_type, "allowed_agents": agents,
|
||
"write_operation": write, "input_schema": ischema, "output_schema": oschema,
|
||
"backend_mapping": backend, "disable_reason": disable_reason,
|
||
"scope_note": scope_note, "sort_no": sort,
|
||
}
|
||
|
||
|
||
TOOL_SEED = [
|
||
# ---------------- 启用(13) ----------------
|
||
_t("blueprint.create", "创建蓝图", "blueprint", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER], 1,
|
||
{"type": "object",
|
||
"required": ["intent_text", "title"],
|
||
"properties": {
|
||
"intent_text": {"type": "string"},
|
||
"title": {"type": "string"},
|
||
"class_id": {"type": "string"},
|
||
"generation_source": {"type": "string",
|
||
"enum": ["ai_generated", "manual",
|
||
"template_fallback", "template_copy"]}}},
|
||
{"type": "object",
|
||
"properties": {"blueprint_id": {"type": "integer"}, "code": {"type": "string"},
|
||
"version_no": {"type": "integer"},
|
||
"generation_source": {"type": "string"},
|
||
"trace_no": {"type": "string"}}},
|
||
"pbl_blueprint.create_blueprint + create_version", 1,
|
||
scope_note="T1;PBL_E_MODEL_UNAVAILABLE 内部切 template.copy 兜底,不向 Agent 抛错"),
|
||
|
||
_t("blueprint.update", "结构化修改蓝图", "blueprint", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER], 1,
|
||
{"type": "object",
|
||
"required": ["blueprint_id", "instruction", "target_changes"],
|
||
"properties": {
|
||
"blueprint_id": {"type": "integer"},
|
||
"instruction": {"type": "string"},
|
||
"target_changes": {"type": "array", "minItems": 1, "items": {
|
||
"type": "object", "required": ["object_type", "field", "value"],
|
||
"properties": {"object_type": {"type": "string"},
|
||
"object_id": {"type": "integer"},
|
||
"field": {"type": "string"},
|
||
"value": {}}}}}},
|
||
{"type": "object",
|
||
"properties": {"new_version": {"type": "integer"},
|
||
"change_delta": {"type": "object"},
|
||
"unaffected_verified": {"type": "boolean"}}},
|
||
"pbl_blueprint.update_sub_object + create_version(change_delta)", 2,
|
||
scope_note="T2;仅目标字段变更,非目标字段字节级不变(US-03 canonical_json diff)"),
|
||
|
||
_t("blueprint.get", "读取蓝图", "blueprint", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER, CRITIC], 0,
|
||
{"type": "object", "required": ["blueprint_id"],
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"version_no": {"type": "integer"},
|
||
"include": {"type": "array", "items": {"type": "string"}}}},
|
||
{"type": "object",
|
||
"properties": {"blueprint": {"type": "object"}, "version": {"type": "object"},
|
||
"sub_objects": {"type": "object"}}},
|
||
"pbl_blueprint.get_blueprint / get_version / get_blueprint_tree", 3,
|
||
scope_note="T3;租户隔离,跨租户返回 PBL_E_NOT_FOUND"),
|
||
|
||
_t("template.list", "模板检索", "template", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER], 0,
|
||
{"type": "object",
|
||
"properties": {"category": {"type": "string"}, "keyword": {"type": "string"},
|
||
"page": {"type": "integer"}, "size": {"type": "integer"}}},
|
||
{"type": "object",
|
||
"properties": {"items": {"type": "array"}, "total": {"type": "integer"}}},
|
||
"pbl_template.list_templates + match_by_intent", 4,
|
||
scope_note="T4;kdb.search 桩返回空时 Designer 降级到本工具(本地模板库)"),
|
||
|
||
_t("template.copy", "模板实例化", "template", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER], 1,
|
||
{"type": "object", "required": ["template_id", "owner_teacher_id"],
|
||
"properties": {"template_id": {"type": "integer"},
|
||
"owner_teacher_id": {"type": "string"},
|
||
"class_id": {"type": "string"},
|
||
"new_title": {"type": "string"}}},
|
||
{"type": "object",
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"version_no": {"type": "integer"},
|
||
"generation_source": {"type": "string"},
|
||
"usage_id": {"type": "integer"}}},
|
||
"pbl_template.copy_template_to_blueprint / fallback_instantiate", 5,
|
||
scope_note="T5;模型不可达兜底落点(generation_source=template_fallback)"),
|
||
|
||
_t("validation.run", "触发校验", "validation", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER], 0,
|
||
{"type": "object", "required": ["blueprint_id", "version_no"],
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"version_no": {"type": "integer"}}},
|
||
{"type": "object",
|
||
"properties": {"run_no": {"type": "string"},
|
||
"quality_status": {"type": "string"},
|
||
"pass_count": {"type": "integer"},
|
||
"warn_count": {"type": "integer"},
|
||
"fail_count": {"type": "integer"}}},
|
||
"pbl_validation.run_validation", 6,
|
||
scope_note="T6;确定性校验,无 LLM"),
|
||
|
||
_t("validation.report", "校验报告读取", "validation", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [DESIGNER, CRITIC], 0,
|
||
{"type": "object",
|
||
"properties": {"run_id": {"type": "integer"},
|
||
"blueprint_id": {"type": "integer"}}},
|
||
{"type": "object",
|
||
"properties": {"run": {"type": "object"}, "dimensions": {"type": "array"},
|
||
"alerts": {"type": "array"}}},
|
||
"pbl_validation.get_validation_report", 7,
|
||
scope_note="T7;Critic 建议证据源(14 维 + 4 内置告警)"),
|
||
|
||
_t("critic.review", "Critic 评审", "critic", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 0, [CRITIC], 0,
|
||
{"type": "object", "required": ["blueprint_id"],
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"version_no": {"type": "integer"}}},
|
||
{"type": "object",
|
||
"properties": {"suggestions": {"type": "array"},
|
||
"trace_no": {"type": "string"}}},
|
||
"pbl_agent_runtime.critic_review(读 T3/T7,零写蓝图)", 8,
|
||
scope_note="T8;write_operation=0,Critic 不直接修改 Blueprint(14.1);"
|
||
"四要素不齐 → PBL_E_VALIDATION(14.3)"),
|
||
|
||
_t("trace.get", "轨迹读取", "trace", TOOL_STATUS_ENABLED,
|
||
PERM_AGENT_TOOLS, 0, [DESIGNER, CRITIC], 0,
|
||
{"type": "object", "required": ["trace_no"],
|
||
"properties": {"trace_no": {"type": "string"}}},
|
||
{"type": "object",
|
||
"properties": {e: {} for e in ("input_context", "retrieved_knowledge",
|
||
"tool_calls", "proposed_action", "result",
|
||
"approval", "final_output")}},
|
||
"pbl_agent_runtime.get_trace(append-only)", 9,
|
||
scope_note="T9;Agent 仅可读自身 trace_no,读他人 → PBL_E_FORBIDDEN"),
|
||
|
||
_t("approval.request", "发起人工审批", "approval", TOOL_STATUS_ENABLED,
|
||
PERM_AGENT_TOOLS, 0, [DESIGNER], 0,
|
||
{"type": "object", "required": ["trace_id", "action_type"],
|
||
"properties": {"trace_id": {"type": "integer"},
|
||
"action_type": {"type": "string",
|
||
"enum": list(APPROVAL_TYPES)},
|
||
"approver_id": {"type": "string"}}},
|
||
{"type": "object",
|
||
"properties": {"approval_no": {"type": "string"},
|
||
"status": {"type": "string", "enum": ["pending"]}}},
|
||
"pbl_agent_runtime.request_approval", 10,
|
||
scope_note="T10;Agent 不能自批——decide_approval 仅 actor_type=user 可调(14.2)"),
|
||
|
||
_t("compile.trigger", "触发编译(需审批)", "compile", TOOL_STATUS_ENABLED,
|
||
PERM_AUTHORING, 1, [DESIGNER], 1,
|
||
{"type": "object",
|
||
"required": ["blueprint_id", "version_no", "compiler_version"],
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"version_no": {"type": "integer"},
|
||
"compiler_version": {"type": "string"}}},
|
||
{"type": "object",
|
||
"properties": {"task_no": {"type": "string"}, "status": {"type": "string"},
|
||
"game_def_id": {"type": "integer"},
|
||
"fingerprint": {"type": "string"}}},
|
||
"pbl_compiler.compile(is_approved 门禁:未审批蓝图 403)", 11,
|
||
approval_type="compile_execute",
|
||
scope_note="T11;require_approval=1,无 approved 记录 → PBL_E_STATE_ILLEGAL"),
|
||
|
||
_t("publish.request", "发布请求(需审批)", "approval", TOOL_STATUS_ENABLED,
|
||
PERM_PUBLISHING, 1, [DESIGNER], 1,
|
||
{"type": "object", "required": ["blueprint_id", "visibility"],
|
||
"properties": {"blueprint_id": {"type": "integer"},
|
||
"visibility": {"type": "string",
|
||
"enum": ["private", "org", "school", "class"]}}},
|
||
{"type": "object",
|
||
"properties": {"approval_no": {"type": "string"},
|
||
"status": {"type": "string"},
|
||
"blueprint_status": {"type": "string"},
|
||
"visibility": {"type": "string"}}},
|
||
"pbl_blueprint.submit_approval(publish_approve) + status 更新", 12,
|
||
approval_type="publish",
|
||
scope_note="T12;Publish 必须人工审批(14.2/36 章);仅可见性标记,无 Marketplace(1.3)"),
|
||
|
||
_t("kdb.search", "知识检索(桩)", "kdb", TOOL_STATUS_ENABLED,
|
||
PERM_KDB, 0, [DESIGNER], 0,
|
||
{"type": "object", "required": ["query"],
|
||
"properties": {"query": {"type": "string"}, "top_k": {"type": "integer"}}},
|
||
{"type": "object",
|
||
"properties": {"items": {"type": "array"}, "total": {"type": "integer"},
|
||
"stub": {"type": "boolean"}}},
|
||
"pbl_kdb_ext.kdb_search", 13,
|
||
scope_note="T13;只读桩,空结果集不报错(Q5/US-24)→ Designer 降级 T4/T5"),
|
||
|
||
# ---------------- 禁用(9,out_of_scope,仍注册在表) ----------------
|
||
_t("blueprint.publish_auto", "全自主发布", "publish", TOOL_STATUS_DISABLED,
|
||
PERM_PUBLISHING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 101,
|
||
disable_reason="全自主发布禁止——Publish 必须人工审批(14.2/28/36 章);"
|
||
"由 publish.request(require_approval=1)替代",
|
||
scope_note="D1 out_of_scope"),
|
||
|
||
_t("curriculum.modify_auto", "自主改课", "blueprint", TOOL_STATUS_DISABLED,
|
||
PERM_AUTHORING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 102,
|
||
disable_reason="自主改课禁止(14.2)——Agent 只 Propose,修改经 blueprint.update "
|
||
"且 Execute 受裁决",
|
||
scope_note="D2 out_of_scope"),
|
||
|
||
_t("marketplace.create_listing", "Marketplace 上架", "marketplace",
|
||
TOOL_STATUS_DISABLED, PERM_PUBLISHING, 0, [], 1,
|
||
{"type": "object", "properties": {}}, {"type": "object", "properties": {}},
|
||
"", 103,
|
||
disable_reason="Marketplace 付费/订阅/分成归 Phase 4(22/23/35 章);"
|
||
"本迭代发布仅可见性标记",
|
||
scope_note="D3 out_of_scope(marketplace.* 全组禁用)"),
|
||
|
||
_t("kdb.write", "KDB 写入", "kdb", TOOL_STATUS_DISABLED, PERM_KDB, 0, [], 1,
|
||
{"type": "object", "properties": {}}, {"type": "object", "properties": {}},
|
||
"", 104,
|
||
disable_reason="KDB 只读(owner Q5)——不建向量库/图谱,禁任何写入",
|
||
scope_note="D4 out_of_scope"),
|
||
|
||
_t("research.collect", "Research 数据采集", "kdb", TOOL_STATUS_DISABLED,
|
||
PERM_KDB, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 105,
|
||
disable_reason="Research data 采集禁止(owner Q6)——任何学生数据不得进入 "
|
||
"research 层;仅匿名聚合只读出口",
|
||
scope_note="D5 out_of_scope"),
|
||
|
||
_t("experiment.ab_run", "A/B 实验执行", "experiments", TOOL_STATUS_DISABLED,
|
||
PERM_AUTHORING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 106,
|
||
disable_reason="自动 A/B 实验引擎归 Phase 3(33/35 章)",
|
||
scope_note="D6 out_of_scope"),
|
||
|
||
_t("agent.mentor_invoke", "Mentor Agent 调用", "agent", TOOL_STATUS_DISABLED,
|
||
PERM_AUTHORING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 107,
|
||
disable_reason="Mentor Agent 归后续迭代(13.1/35 章 Phase 2)——本迭代仅 "
|
||
"Designer+Critic",
|
||
scope_note="D7 out_of_scope"),
|
||
|
||
_t("world.edit_3d", "3D 编辑器", "world", TOOL_STATUS_DISABLED,
|
||
PERM_AUTHORING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 108,
|
||
disable_reason="3D 编辑器禁止(owner Q1 / 35 章 Phase 0 Do not build)——"
|
||
"JSON 驱动渲染替代",
|
||
scope_note="D8 out_of_scope"),
|
||
|
||
_t("billing.charge", "计费扣款", "marketplace", TOOL_STATUS_DISABLED,
|
||
PERM_PUBLISHING, 0, [], 1, {"type": "object", "properties": {}},
|
||
{"type": "object", "properties": {}}, "", 109,
|
||
disable_reason="计费/SSO/企业私有部署归 Phase 4(23.3)",
|
||
scope_note="D9 out_of_scope(billing.* 全组禁用)"),
|
||
]
|
||
|
||
ENABLED_TOOL_CODES = tuple(t["tool_code"] for t in TOOL_SEED
|
||
if t["status"] == TOOL_STATUS_ENABLED)
|
||
DISABLED_TOOL_CODES = tuple(t["tool_code"] for t in TOOL_SEED
|
||
if t["status"] == TOOL_STATUS_DISABLED)
|
||
|
||
# 需强制人工审批的工具(require_approval=1)
|
||
APPROVAL_REQUIRED_TOOLS = tuple(t["tool_code"] for t in TOOL_SEED
|
||
if t["require_approval"])
|
||
|
||
|
||
def _require_platform_admin(ctx):
|
||
"""注册表治理仅 Platform Admin(agent-tool-contract.md §4.6)。"""
|
||
if ctx is None or not ctx.has_perm(PERM_PLATFORM_ADMIN):
|
||
fail(E_FORBIDDEN, "工具注册表治理仅 Platform Admin 可操作",
|
||
actor=getattr(ctx, "actor_id", None))
|
||
if ctx.actor_type != ACTOR_USER:
|
||
fail(E_FORBIDDEN, "工具注册表治理仅人类用户可操作(Agent 不可自改注册表)",
|
||
actor_type=ctx.actor_type)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. Agent 定义 seed / 查询
|
||
# ---------------------------------------------------------------------------
|
||
def seed_agents(ctx=None, store=None):
|
||
"""幂等注入 designer / critic 定义(按 tenant_id+agent_code upsert)。"""
|
||
st = store or get_store()
|
||
tenant_id = (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
inserted, skipped = 0, 0
|
||
for d in AGENT_DEFS:
|
||
row = {
|
||
"tenant_id": tenant_id,
|
||
"agent_code": d["agent_code"],
|
||
"agent_name": d["agent_name"],
|
||
"agent_type": d["agent_type"],
|
||
"loop_pattern": d["loop_pattern"],
|
||
"authority_boundary": d["authority_boundary"],
|
||
"write_allowed": d["write_allowed"],
|
||
"allowed_tools": dumps(d["allowed_tools"]),
|
||
"denied_tools": dumps(d["denied_tools"]),
|
||
"model_binding": dumps(d["model_binding"]),
|
||
"offline_fallback": d["offline_fallback"],
|
||
"status": d["status"],
|
||
"description": d["description"],
|
||
"created_by": "seed",
|
||
"created_at": now_ts(),
|
||
"updated_at": now_ts(),
|
||
}
|
||
exist = one(st, "pbl_agent_def",
|
||
{"tenant_id": tenant_id, "agent_code": d["agent_code"]})
|
||
if exist:
|
||
st.U("pbl_agent_def", row,
|
||
{"tenant_id": tenant_id, "agent_code": d["agent_code"]})
|
||
skipped += 1
|
||
else:
|
||
st.C("pbl_agent_def", row)
|
||
inserted += 1
|
||
return {"inserted": inserted, "skipped": skipped, "agents": [d["agent_code"] for d in AGENT_DEFS]}
|
||
|
||
|
||
def get_agent_def(agent_code, ctx=None, store=None):
|
||
st = store or get_store()
|
||
tenant_id = (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
row = one(st, "pbl_agent_def", {"tenant_id": tenant_id, "agent_code": agent_code})
|
||
if row is None:
|
||
# 回落到平台级定义(seed 用 __platform__ 时)
|
||
row = one(st, "pbl_agent_def",
|
||
{"tenant_id": "__platform__", "agent_code": agent_code})
|
||
if row is None:
|
||
fail(E_NOT_FOUND, "Agent 定义不存在: %s" % agent_code, agent_code=agent_code)
|
||
row = dict(row)
|
||
row["allowed_tools"] = loads(row.get("allowed_tools"), []) or []
|
||
row["denied_tools"] = loads(row.get("denied_tools"), []) or []
|
||
row["model_binding"] = loads(row.get("model_binding"), {}) or {}
|
||
return row
|
||
|
||
|
||
def list_agents(ctx=None, store=None):
|
||
st = store or get_store()
|
||
tenant_id = (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
rows = st.R("pbl_agent_def", {"tenant_id": tenant_id}, order_by="id")
|
||
if not rows:
|
||
rows = st.R("pbl_agent_def", {"tenant_id": "__platform__"}, order_by="id")
|
||
out = []
|
||
for r in rows:
|
||
r = dict(r)
|
||
r["allowed_tools"] = loads(r.get("allowed_tools"), []) or []
|
||
r["denied_tools"] = loads(r.get("denied_tools"), []) or []
|
||
out.append(r)
|
||
return out
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. 工具注册表 CRUD
|
||
# ---------------------------------------------------------------------------
|
||
def register_tool(ctx, tool_code, tool_name, tool_group, status,
|
||
required_permission, require_approval=0, input_schema=None,
|
||
output_schema=None, disable_reason=None, allowed_agents=None,
|
||
write_operation=0, backend_mapping="", approval_action_type=None,
|
||
scope_note=None, store=None):
|
||
"""
|
||
注册工具(仅 Platform Admin)。重复 tool_code → PBL_E_DUPLICATE。
|
||
disabled 必须给 disable_reason(审计可解释)。
|
||
"""
|
||
from .m4a_kernel import require_tenant
|
||
tenant_id = require_tenant(ctx)
|
||
_require_platform_admin(ctx)
|
||
|
||
if not tool_code or not isinstance(tool_code, str) or "." not in tool_code:
|
||
from .m4a_kernel import E_VALIDATION
|
||
fail(E_VALIDATION, "tool_code 非法(须为 group.name 形式)", tool_code=tool_code)
|
||
if status not in (TOOL_STATUS_ENABLED, TOOL_STATUS_DISABLED):
|
||
from .m4a_kernel import E_VALIDATION
|
||
fail(E_VALIDATION, "status 仅 enabled/disabled", status=status)
|
||
if status == TOOL_STATUS_DISABLED and not disable_reason:
|
||
from .m4a_kernel import E_VALIDATION
|
||
fail(E_VALIDATION, "禁用工具必须填写 disable_reason(out_of_scope 锚点)",
|
||
tool_code=tool_code)
|
||
|
||
st = store or get_store()
|
||
if one(st, "pbl_agent_tool", {"tenant_id": tenant_id, "tool_code": tool_code}):
|
||
fail(E_DUPLICATE, "工具已注册: %s" % tool_code, tool_code=tool_code)
|
||
|
||
row = {
|
||
"tenant_id": tenant_id, "tool_code": tool_code, "tool_name": tool_name,
|
||
"tool_group": tool_group, "status": status,
|
||
"required_permission": required_permission,
|
||
"require_approval": 1 if require_approval else 0,
|
||
"approval_action_type": approval_action_type,
|
||
"allowed_agents": dumps(allowed_agents or []),
|
||
"write_operation": 1 if write_operation else 0,
|
||
"input_schema": dumps(input_schema or {"type": "object", "properties": {}}),
|
||
"output_schema": dumps(output_schema or {"type": "object", "properties": {}}),
|
||
"backend_mapping": backend_mapping or "",
|
||
"disable_reason": disable_reason, "scope_note": scope_note,
|
||
"sort_no": 900, "created_by": ctx.actor_id, "created_at": now_ts(),
|
||
"updated_at": now_ts(),
|
||
}
|
||
tid = st.C("pbl_agent_tool", row)
|
||
write_audit(ctx, "tool.register", "pbl_agent_tool", tid,
|
||
{"tool_code": tool_code, "status": status}, store=st)
|
||
row["id"] = tid
|
||
return _decode_tool(row)
|
||
|
||
|
||
def _decode_tool(row):
|
||
r = dict(row)
|
||
r["allowed_agents"] = loads(r.get("allowed_agents"), []) or []
|
||
r["input_schema"] = loads(r.get("input_schema"), {}) or {}
|
||
r["output_schema"] = loads(r.get("output_schema"), {}) or {}
|
||
return r
|
||
|
||
|
||
def get_tool(tool_code, ctx=None, store=None, tenant_id=None):
|
||
"""取工具注册行(含禁用行)。找不到 → None(由裁决链 S3 判 default-deny)。"""
|
||
st = store or get_store()
|
||
tid = tenant_id or (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
row = one(st, "pbl_agent_tool", {"tenant_id": tid, "tool_code": tool_code})
|
||
if row is None and tid != "__platform__":
|
||
row = one(st, "pbl_agent_tool",
|
||
{"tenant_id": "__platform__", "tool_code": tool_code})
|
||
return _decode_tool(row) if row else None
|
||
|
||
|
||
def list_tools(ctx=None, status=None, group=None, store=None, tenant_id=None):
|
||
st = store or get_store()
|
||
tid = tenant_id or (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
rows = st.R("pbl_agent_tool", {"tenant_id": tid}, order_by="sort_no")
|
||
if not rows:
|
||
rows = st.R("pbl_agent_tool", {"tenant_id": "__platform__"}, order_by="sort_no")
|
||
out = [_decode_tool(r) for r in rows]
|
||
if status:
|
||
out = [t for t in out if t.get("status") == status]
|
||
if group:
|
||
out = [t for t in out if t.get("tool_group") == group]
|
||
return out
|
||
|
||
|
||
def set_tool_status(ctx, tool_code, status, reason=None, store=None):
|
||
"""
|
||
启停工具(仅 Platform Admin,人类)。属四类强制审批之 tool_registry_change:
|
||
启用一个此前禁用的工具必须携带 approved 审批单号(reason 传 approval_no)。
|
||
"""
|
||
from .m4a_kernel import require_tenant, E_VALIDATION, E_STATE_ILLEGAL
|
||
tenant_id = require_tenant(ctx)
|
||
_require_platform_admin(ctx)
|
||
if status not in (TOOL_STATUS_ENABLED, TOOL_STATUS_DISABLED):
|
||
fail(E_VALIDATION, "status 仅 enabled/disabled", status=status)
|
||
|
||
st = store or get_store()
|
||
row = get_tool(tool_code, ctx=ctx, store=st, tenant_id=tenant_id)
|
||
if row is None:
|
||
fail(E_NOT_FOUND, "工具未注册: %s" % tool_code, tool_code=tool_code)
|
||
|
||
# 禁用→启用:强制人工审批(tool_registry_change)
|
||
if row["status"] == TOOL_STATUS_DISABLED and status == TOOL_STATUS_ENABLED:
|
||
from .m4a_approval import find_approved
|
||
ap = find_approved("tool_registry_change", tool_code, ctx=ctx, store=st,
|
||
tenant_id=tenant_id, approval_no=reason)
|
||
if ap is None:
|
||
fail(E_STATE_ILLEGAL,
|
||
"启用禁用工具属 tool_registry_change 强制人工审批,缺少 approved 审批单",
|
||
tool_code=tool_code, action_type="tool_registry_change")
|
||
|
||
if status == TOOL_STATUS_DISABLED and not reason:
|
||
fail(E_VALIDATION, "禁用工具必须给出原因(写入 disable_reason)",
|
||
tool_code=tool_code)
|
||
|
||
patch = {"status": status, "updated_at": now_ts()}
|
||
if status == TOOL_STATUS_DISABLED:
|
||
patch["disable_reason"] = reason
|
||
st.U("pbl_agent_tool", patch, {"tenant_id": tenant_id, "tool_code": tool_code})
|
||
write_audit(ctx, "tool.set_status", "pbl_agent_tool", row.get("id"),
|
||
{"tool_code": tool_code, "from": row["status"], "to": status,
|
||
"reason": reason}, store=st)
|
||
return _decode_tool(get_tool(tool_code, ctx=ctx, store=st, tenant_id=tenant_id))
|
||
|
||
|
||
def seed_tools(ctx=None, store=None):
|
||
"""
|
||
幂等注入 13 enabled + 9 disabled(build.sh 调用)。
|
||
按 tenant_id + tool_code upsert:已存在则更新契约字段,不重复插入。
|
||
"""
|
||
st = store or get_store()
|
||
tenant_id = (ctx.tenant_id if ctx is not None else None) or "__platform__"
|
||
inserted, skipped = 0, 0
|
||
for t in TOOL_SEED:
|
||
row = {
|
||
"tenant_id": tenant_id,
|
||
"tool_code": t["tool_code"], "tool_name": t["tool_name"],
|
||
"tool_group": t["tool_group"], "status": t["status"],
|
||
"required_permission": t["required_permission"],
|
||
"require_approval": t["require_approval"],
|
||
"approval_action_type": t.get("approval_action_type"),
|
||
"allowed_agents": dumps(t["allowed_agents"]),
|
||
"write_operation": t["write_operation"],
|
||
"input_schema": dumps(t["input_schema"]),
|
||
"output_schema": dumps(t["output_schema"]),
|
||
"backend_mapping": t["backend_mapping"],
|
||
"disable_reason": t.get("disable_reason"),
|
||
"scope_note": t.get("scope_note"),
|
||
"sort_no": t["sort_no"], "created_by": "seed",
|
||
"created_at": now_ts(), "updated_at": now_ts(),
|
||
}
|
||
exist = one(st, "pbl_agent_tool",
|
||
{"tenant_id": tenant_id, "tool_code": t["tool_code"]})
|
||
if exist:
|
||
st.U("pbl_agent_tool", row,
|
||
{"tenant_id": tenant_id, "tool_code": t["tool_code"]})
|
||
skipped += 1
|
||
else:
|
||
st.C("pbl_agent_tool", row)
|
||
inserted += 1
|
||
return {
|
||
"inserted": inserted, "skipped": skipped,
|
||
"enabled": len(ENABLED_TOOL_CODES), "disabled": len(DISABLED_TOOL_CODES),
|
||
"total": len(TOOL_SEED),
|
||
"enabled_codes": list(ENABLED_TOOL_CODES),
|
||
"disabled_codes": list(DISABLED_TOOL_CODES),
|
||
"approval_required": list(APPROVAL_REQUIRED_TOOLS),
|
||
}
|
||
|
||
|
||
def registry_stats(ctx=None, store=None):
|
||
"""注册表统计(供前端/审计核对 13+9)。"""
|
||
tools = list_tools(ctx=ctx, store=store)
|
||
en = [t for t in tools if t["status"] == TOOL_STATUS_ENABLED]
|
||
dis = [t for t in tools if t["status"] == TOOL_STATUS_DISABLED]
|
||
return {
|
||
"total": len(tools), "enabled": len(en), "disabled": len(dis),
|
||
"approval_required": [t["tool_code"] for t in en if t.get("require_approval")],
|
||
"disabled_missing_reason": [t["tool_code"] for t in dis
|
||
if not t.get("disable_reason")],
|
||
"critic_writable_tools": [t["tool_code"] for t in en
|
||
if t.get("write_operation")
|
||
and CRITIC in (t.get("allowed_agents") or [])],
|
||
}
|