2026-09-17 23:44:00 +08:00

639 lines
31 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.

# -*- coding: utf-8 -*-
"""
pbl_agent_runtime M4a — Agent 定义与工具注册表(含 seed 幂等注入)
内容:
1) AGENT_DEFS第 13.1 章仅 designer / critic 两个 Agentcritic write_allowed=0零写权限
2) TOOL_SEED第 31 章裁剪子集 —— 13 enabled + 9 disabledagent-tool-contract.md §2/§3
3) register_tool / list_tools / set_tool_status / seed_tools / seed_agents幂等build.sh 调用)
四类强制人工审批14.2
publishT12 publish.request/ compile_executeT11 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.1Designer 只 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 / confidence14.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. 工具注册表 seed13 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="T1PBL_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="T4kdb.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="T7Critic 建议证据源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="T8write_operation=0Critic 不直接修改 Blueprint14.1"
"四要素不齐 → PBL_E_VALIDATION14.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_traceappend-only", 9,
scope_note="T9Agent 仅可读自身 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="T10Agent 不能自批——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.compileis_approved 门禁:未审批蓝图 403", 11,
approval_type="compile_execute",
scope_note="T11require_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="T12Publish 必须人工审批14.2/36 章);仅可见性标记,无 Marketplace1.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"),
# ---------------- 禁用9out_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.requestrequire_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 422/23/35 章);"
"本迭代发布仅可见性标记",
scope_note="D3 out_of_scopemarketplace.* 全组禁用)"),
_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 333/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 423.3",
scope_note="D9 out_of_scopebilling.* 全组禁用)"),
]
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 Adminagent-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_reasonout_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 disabledbuild.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 [])],
}