2026-09-18 14:59:02 +08:00

1177 lines
54 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_contract —— 10 个契约接口的**唯一实现**。
契约清单(与 skill/SKILL.md、wwwroot/api/*.dspy、scripts/load_path.py 四处同步):
pbl_agent_designer_run / pbl_agent_critic_run
pbl_agent_trace_write / pbl_agent_trace_list
pbl_tool_registry_list / pbl_tool_registry_save
pbl_tool_adjudicate
pbl_approval_create / pbl_approval_decide / pbl_approval_list
三条铁律
--------
* **fail-closed**:默认裁决 = DENY任一步不过即 DENY存储/参数异常一律 DENY不放行。
* **Critic 零写**critic 的 write_scope='none'、can_write=0任何 write_class=1 工具在 S3 直接 DENY。
* **四类强制人工审批**publish / compile_execute / blueprint_approve / tool_registry_change
无 approved 审批单即 DENY且自动补建 pending 审批(无绕过路径)。
所有函数**同步返回 Result(dict)**,同时 ``await`` 亦可用Result.__await__ 返回自身),
因此 .dspyasync 上下文)与 offline_contract_smoke.py同步调用共用同一实现。
"""
from __future__ import annotations
import time
from .m4a_store import (
APPEND_ONLY_TABLES,
ERR_APPEND_ONLY,
ERR_NOT_FOUND,
ERR_STORE,
ERR_TENANT_MISSING,
PblContractError,
Result,
dumps,
fail,
get_store,
loads,
new_id,
now_ms,
now_str,
ok,
resolve_tenant,
sha256_text,
)
# ---------------------------------------------------------------------------
# 契约常量
# ---------------------------------------------------------------------------
CONTRACT_FUNCTIONS = (
"pbl_agent_designer_run",
"pbl_agent_critic_run",
"pbl_agent_trace_write",
"pbl_agent_trace_list",
"pbl_tool_registry_list",
"pbl_tool_registry_save",
"pbl_tool_adjudicate",
"pbl_approval_create",
"pbl_approval_decide",
"pbl_approval_list",
)
# 14.2 四类强制人工审批(无绕过路径)
MANDATORY_APPROVAL_TYPES = (
"publish",
"compile_execute",
"blueprint_approve",
"tool_registry_change",
)
# fail-closed 8 步裁决顺序(顺序不可调换:先身份、后授权、再参数、最后审批)
ADJUDICATION_STEPS = (
("S1", "tenant_context", "租户上下文必须存在"),
("S2", "agent_registered", "Agent 必须已注册且 active"),
("S3", "agent_write_scope", "Critic 零写:写类工具对 critic 一律 DENY"),
("S4", "tool_registered", "工具必须在注册表中登记"),
("S5", "tool_enabled", "工具必须处于 enabledpbl.publish 等禁用工具 DENY"),
("S6", "permission_granted", "Agent 必须持有工具 required_perm"),
("S7", "params_valid", "入参必须通过 params_schema 校验"),
("S8", "approval_granted", "四类强制审批必须有 approved 审批单"),
)
TRACE_ELEMENTS = ("who", "occurred_at", "what", "why", "how", "result", "evidence_ref")
# 13 启用工具write_class=1 为写类audit_append=1 表示只追加留痕)
ENABLED_TOOLS = (
("blueprint.read", "蓝图读取", "blueprint", 0, "pbl.blueprint.read", None, "low"),
("blueprint.write", "蓝图写入", "blueprint", 1, "pbl.blueprint.write", None, "high"),
("blueprint.validate", "蓝图校验", "blueprint", 0, "pbl.blueprint.read", None, "low"),
("blueprint.approve", "蓝图审批", "blueprint", 1, "pbl.blueprint.approve", "blueprint_approve", "high"),
("compiler.compile", "确定性编译", "compiler", 0, "pbl.compiler.run", None, "medium"),
("compiler.execute", "编译产物执行", "compiler", 1, "pbl.compiler.execute", "compile_execute", "high"),
("evidence.collect", "证据采集", "evidence", 1, "pbl.evidence.write", None, "medium"),
("assessment.score", "Rubric 评分", "assessment", 1, "pbl.assessment.write", None, "medium"),
("kdb.query", "KDB 只读查询", "kdb", 0, "pbl.kdb.read", None, "low"),
("runtime.event", "运行时事件写入", "runtime", 1, "pbl.runtime.write", None, "medium"),
("agent.trace_write", "轨迹留痕", "agent", 1, "pbl.agent.trace", None, "low"),
("approval.create", "发起人工审批", "approval", 1, "pbl.approval.create", None, "medium"),
("tool_registry.read", "工具注册表查询", "registry", 0, "pbl.registry.read", None, "low"),
)
# 9 禁用工具(含 pbl.publish 自动发布类,一律 status=disabled
DISABLED_TOOLS = (
("pbl.publish", "自动发布上线", "publish", 1, "pbl.publish", "publish", "forbidden", "14.2 发布必须人工审批,禁止 Agent 自动发布"),
("blueprint.publish_auto", "蓝图自动发布", "publish", 1, "pbl.publish", "publish", "forbidden", "自动发布类工具fail-closed 默认禁用"),
("compiler.autorun", "编译自动执行", "compiler", 1, "pbl.compiler.execute", "compile_execute", "forbidden", "执行必须人工审批"),
("kdb.write", "KDB 写入", "kdb", 1, "pbl.kdb.write", None, "forbidden", "M7 KDB 只读桩,零写入"),
("tenant.switch", "跨租户切换", "platform", 1, "pbl.platform.admin", None, "forbidden", "跨租户越权,禁止 Agent 调用"),
("rbac.grant", "权限授予", "platform", 1, "pbl.platform.admin", None, "forbidden", "权限变更必须人工,禁止 Agent 自授权"),
("approval.auto_decide", "审批自动通过", "approval", 1, "pbl.approval.decide", "blueprint_approve", "forbidden", "审批无绕过路径,禁止自动通过"),
("trace.delete", "轨迹删除", "agent", 1, "pbl.platform.admin", None, "forbidden", "留痕 append-only禁止删除"),
("llm.raw_exec", "模型原始指令执行", "agent", 1, "pbl.platform.admin", None, "forbidden", "禁止把模型输出当指令直接执行"),
)
# Agent 定义designer 可写、critic 零写
AGENT_DEFS = (
{
"agent_code": "designer",
"agent_name": "Designer 设计者 Agent",
"role": "designer",
"write_scope": "blueprint,evidence,runtime,assessment",
"can_write": 1,
# 注意:持有权限 != 可自动执行。blueprint.approve / compiler.execute 虽在 perms 内,
# 仍必须在 S8 命中四类强制人工审批blueprint_approve / compile_execute才 ALLOW。
"perms": [
"pbl.blueprint.read", "pbl.blueprint.write", "pbl.blueprint.approve",
"pbl.compiler.run", "pbl.compiler.execute",
"pbl.evidence.write", "pbl.assessment.write", "pbl.runtime.write",
"pbl.kdb.read", "pbl.agent.trace", "pbl.approval.create", "pbl.registry.read",
],
"model": "platform-default",
"system_prompt": "你是 PBL 蓝图 Designer只做确定性设计产出所有写操作必须经服务端裁决"
"发布/执行/审批/注册表变更四类动作必须发起人工审批,禁止自行绕过。",
},
{
"agent_code": "critic",
"agent_name": "Critic 评审 Agent",
"role": "critic",
"write_scope": "none",
"can_write": 0,
"perms": ["pbl.blueprint.read", "pbl.kdb.read", "pbl.agent.trace", "pbl.registry.read"],
"model": "platform-default",
"system_prompt": "你是 PBL Critic只读评审零写权限。任何写类工具调用都会被服务端在 S3 拒绝;"
"你只输出评审意见与证据引用,不得修改蓝图、不得发布、不得审批。",
},
)
DEFAULT_TIMEOUT_MS = 30000
DEFAULT_MAX_RETRY = 2
# ---------------------------------------------------------------------------
# 内部工具
# ---------------------------------------------------------------------------
def _tenant_or_raise(tenant_id):
tenant = resolve_tenant(tenant_id)
if not tenant:
raise PblContractError(ERR_TENANT_MISSING, "tenant_id missing (fail-closed)")
return tenant
def _pick(kwargs: dict, names, default=None):
for name in names:
if name in kwargs and kwargs[name] is not None:
return kwargs[name]
return default
def _as_bool(value, default=False) -> bool:
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return bool(value)
text = str(value).strip().lower()
if text in ("1", "true", "yes", "y", "on", "enabled", "enable"):
return True
if text in ("0", "false", "no", "n", "off", "disabled", "disable"):
return False
return default
def _as_int(value, default=0) -> int:
try:
return int(value)
except Exception:
return default
def _guard(kwargs: dict):
"""契约统一异常包装:把 PblContractError 转成 fail Result其它异常转 ERR_STORE。"""
try:
return kwargs["fn"](**kwargs["kw"])
except PblContractError as exc:
return fail(exc.code, exc.message, **exc.extra)
except Exception as exc: # noqa: BLE001 —— fail-closed未知异常也拒绝
return fail(ERR_STORE, "contract error: %s: %s" % (type(exc).__name__, exc))
def _run(fn, **kw):
return _guard({"fn": fn, "kw": kw})
# ---------------------------------------------------------------------------
# LLM 调用留痕pbl_llm_call_log超时 / 重试 / 限流 / 兜底
# ---------------------------------------------------------------------------
def log_llm_call(
tenant_id=None,
agent_code=None,
run_id=None,
purpose=None,
model=None,
attempt_no=1,
status="ok",
latency_ms=0,
timeout_ms=DEFAULT_TIMEOUT_MS,
retry_count=0,
rate_limited=False,
fallback_used=False,
prompt=None,
response=None,
tokens_in=0,
tokens_out=0,
error_code=None,
error_msg=None,
) -> dict:
"""写一条 LLM 调用留痕append-only。Designer/Critic 运行时每次模型调用都必须落一条。"""
tenant = _tenant_or_raise(tenant_id)
prompt_text = prompt if isinstance(prompt, str) else dumps(prompt)
response_text = response if isinstance(response, str) else dumps(response)
row = get_store().insert(
"pbl_llm_call_log",
{
"tenant_id": tenant,
"agent_code": agent_code or "unknown",
"run_id": run_id,
"purpose": purpose or "chat",
"model": model or "platform-default",
"attempt_no": _as_int(attempt_no, 1),
"status": status or "ok",
"latency_ms": _as_int(latency_ms, 0),
"timeout_ms": _as_int(timeout_ms, DEFAULT_TIMEOUT_MS),
"retry_count": _as_int(retry_count, 0),
"rate_limited": 1 if _as_bool(rate_limited) else 0,
"fallback_used": 1 if _as_bool(fallback_used) else 0,
"prompt_chars": len(prompt_text or ""),
"prompt_hash": sha256_text(prompt_text or ""),
"response_chars": len(response_text or ""),
"tokens_in": _as_int(tokens_in, 0),
"tokens_out": _as_int(tokens_out, 0),
"error_code": error_code,
"error_msg": (error_msg or "")[:500] or None,
"created_at": now_str(),
},
)
return row
def _invoke_llm(tenant, agent_code, run_id, purpose, prompt, model=None,
timeout_ms=DEFAULT_TIMEOUT_MS, max_retry=DEFAULT_MAX_RETRY):
"""离线确定性“模型调用”:不联网、不编造,返回可复现的确定性产出,并全程留痕。
真实部署时把本函数体替换为平台模型调用即可——留痕/超时/重试/限流/兜底语义保持不变。
"""
started = now_ms()
attempt = 0
last_error = None
while attempt <= max_retry:
attempt += 1
try:
text = _deterministic_completion(agent_code, purpose, prompt)
latency = now_ms() - started
log_llm_call(
tenant_id=tenant, agent_code=agent_code, run_id=run_id, purpose=purpose,
model=model or "platform-default", attempt_no=attempt, status="ok",
latency_ms=latency, timeout_ms=timeout_ms, retry_count=attempt - 1,
rate_limited=False, fallback_used=False, prompt=prompt, response=text,
tokens_in=max(1, len(str(prompt or "")) // 4),
tokens_out=max(1, len(text) // 4),
)
return {"ok": True, "text": text, "attempt_no": attempt, "latency_ms": latency,
"fallback_used": False, "retry_count": attempt - 1}
except Exception as exc: # noqa: BLE001
last_error = exc
rate_limited = "429" in str(exc) or "rate" in str(exc).lower()
log_llm_call(
tenant_id=tenant, agent_code=agent_code, run_id=run_id, purpose=purpose,
model=model or "platform-default", attempt_no=attempt,
status="rate_limited" if rate_limited else "error",
latency_ms=now_ms() - started, timeout_ms=timeout_ms,
retry_count=attempt - 1, rate_limited=rate_limited, fallback_used=False,
prompt=prompt, error_code="PBL.LLM.CALL_FAILED",
error_msg="%s: %s" % (type(exc).__name__, exc),
)
# 兜底:全部重试失败 → 留痕 fallback_used=1返回确定性兜底产出绝不静默成功
fallback_text = _fallback_completion(agent_code, purpose)
log_llm_call(
tenant_id=tenant, agent_code=agent_code, run_id=run_id, purpose=purpose,
model=model or "platform-default", attempt_no=attempt, status="fallback",
latency_ms=now_ms() - started, timeout_ms=timeout_ms, retry_count=max_retry,
rate_limited=False, fallback_used=True, prompt=prompt, response=fallback_text,
error_code="PBL.LLM.FALLBACK", error_msg=str(last_error)[:500],
)
return {"ok": False, "text": fallback_text, "attempt_no": attempt,
"latency_ms": now_ms() - started, "fallback_used": True, "retry_count": max_retry,
"error_msg": str(last_error)[:500]}
def _deterministic_completion(agent_code, purpose, prompt) -> str:
digest = sha256_text("%s|%s|%s" % (agent_code, purpose, prompt))[:12]
if agent_code == "critic":
return ("CRITIC_REVIEW digest=%s verdict=needs_evidence "
"findings=[\"写操作零权限已核验\",\"证据引用需补全\"] "
"note=critic 只读,不产出任何写入指令" % digest)
return ("DESIGNER_OUTPUT digest=%s purpose=%s "
"steps=[\"读取蓝图\",\"确定性生成子对象\",\"提交校验\"] "
"guard=所有写操作经服务端 8 步裁决" % (digest, purpose))
def _fallback_completion(agent_code, purpose) -> str:
return "FALLBACK agent=%s purpose=%s reason=llm_unavailable action=hold_and_report" % (
agent_code, purpose)
# ---------------------------------------------------------------------------
# 注册表seed / list / save
# ---------------------------------------------------------------------------
def seed_registry(tenant_id=None, force=False) -> dict:
"""幂等 seed2 个 Agent 定义 + 13 启用 / 9 禁用工具。"""
tenant = _tenant_or_raise(tenant_id)
store = get_store()
agents_written = 0
for item in AGENT_DEFS:
existed = store.query_one(
"SELECT id FROM pbl_agent_def WHERE tenant_id = ? AND agent_code = ?",
(tenant, item["agent_code"]),
)
if existed and not force:
continue
store.upsert_by(
"pbl_agent_def",
{"tenant_id": tenant, "agent_code": item["agent_code"]},
{
"agent_name": item["agent_name"],
"role": item["role"],
"write_scope": item["write_scope"],
"can_write": item["can_write"],
"perms": dumps(item["perms"]),
"model": item["model"],
"system_prompt": item["system_prompt"],
"status": "active",
"updated_at": now_str(),
},
)
agents_written += 1
tools_written = 0
sort_no = 0
for key, name, category, write_class, perm, approval, risk in ENABLED_TOOLS:
sort_no += 10
store.upsert_by(
"pbl_tool_registry",
{"tenant_id": tenant, "tool_key": key},
{
"tool_name": name, "category": category, "write_class": write_class,
"audit_append": 1 if key in ("agent.trace_write", "evidence.collect") else 0,
"status": "enabled", "required_perm": perm, "approval_type": approval,
"params_schema": dumps({"type": "object"}), "risk_level": risk,
"reason": None, "sort_no": sort_no, "updated_at": now_str(),
},
)
tools_written += 1
for key, name, category, write_class, perm, approval, risk, reason in DISABLED_TOOLS:
sort_no += 10
store.upsert_by(
"pbl_tool_registry",
{"tenant_id": tenant, "tool_key": key},
{
"tool_name": name, "category": category, "write_class": write_class,
"audit_append": 0, "status": "disabled", "required_perm": perm,
"approval_type": approval, "params_schema": dumps({"type": "object"}),
"risk_level": risk, "reason": reason, "sort_no": sort_no,
"updated_at": now_str(),
},
)
tools_written += 1
return {"tenant_id": tenant, "agents": agents_written, "tools": tools_written,
"enabled": len(ENABLED_TOOLS), "disabled": len(DISABLED_TOOLS)}
def _tool_row_to_dict(row: dict) -> dict:
out = dict(row)
out["params_schema"] = loads(out.get("params_schema"), {"type": "object"})
out["enabled"] = str(out.get("status") or "").lower() == "enabled"
out["write_class"] = _as_int(out.get("write_class"), 0)
out["audit_append"] = _as_int(out.get("audit_append"), 0)
return out
def _pbl_tool_registry_list(tenant_id=None, status=None, category=None, tool_key=None,
keyword=None, page=1, page_size=100, **_ignored) -> Result:
tenant = _tenant_or_raise(tenant_id)
seed_registry(tenant)
where = ["tenant_id = ?"]
args = [tenant]
if status:
where.append("status = ?")
args.append(str(status).strip().lower())
if category:
where.append("category = ?")
args.append(str(category))
if tool_key:
where.append("tool_key = ?")
args.append(str(tool_key))
if keyword:
where.append("(tool_key LIKE ? OR tool_name LIKE ?)")
args.extend(["%%%s%%" % keyword, "%%%s%%" % keyword])
where_sql = " AND ".join(where)
total_row = get_store().query_one(
"SELECT COUNT(1) AS cnt FROM pbl_tool_registry WHERE %s" % where_sql, args)
total = _as_int((total_row or {}).get("cnt"), 0)
page = max(1, _as_int(page, 1))
page_size = min(500, max(1, _as_int(page_size, 100)))
rows = get_store().query(
"SELECT * FROM pbl_tool_registry WHERE %s ORDER BY sort_no, tool_key LIMIT ? OFFSET ?"
% where_sql,
args + [page_size, (page - 1) * page_size],
)
items = [_tool_row_to_dict(r) for r in rows]
enabled = len([i for i in items if i["enabled"]])
return ok(items=items, data=items, list=items, total=total, page=page,
page_size=page_size, tenant_id=tenant,
enabled_count=enabled, disabled_count=len(items) - enabled)
def _pbl_tool_registry_save(tenant_id=None, tool_key=None, tool_name=None, category=None,
enabled=None, status=None, write_class=None, audit_append=None,
required_perm=None, approval_type=None, params_schema=None,
risk_level=None, reason=None, sort_no=None, operator=None,
approval_id=None, **_ignored) -> Result:
"""注册表变更 = 四类强制审批之一tool_registry_change
fail-closed
* tool_key 缺失 → 拒绝;
* 无 approved 的 tool_registry_change 审批单 → 拒绝并自动补建 pending 审批;
* 试图把禁用类pbl.publish 等 forbidden改为 enabled → 拒绝;
* 变更成功后追加 pbl_agent_trace 留痕append-only
"""
tenant = _tenant_or_raise(tenant_id)
seed_registry(tenant)
key = (tool_key or "").strip()
if not key:
return fail("PBL.REGISTRY.TOOL_KEY_REQUIRED", "tool_key is required")
forbidden_keys = {item[0] for item in DISABLED_TOOLS}
want_enabled = _as_bool(enabled, None) if enabled is not None else None
if status is not None and want_enabled is None:
want_enabled = str(status).strip().lower() == "enabled"
if want_enabled is True and key in forbidden_keys:
return fail("PBL.REGISTRY.FORBIDDEN_TOOL",
"tool %s is forbidden by design (14.2), cannot be enabled" % key,
tool_key=key)
approval_row = None
if approval_id:
approval_row = get_store().query_one(
"SELECT * FROM pbl_approval WHERE tenant_id = ? AND approval_id = ?",
(tenant, str(approval_id)),
)
if not approval_row or str(approval_row.get("status")) != "approved":
created = _pbl_approval_create(
tenant_id=tenant, approval_type="tool_registry_change",
title="工具注册表变更:%s" % key, requested_by=operator or "system",
agent_code="platform", tool_key=key,
payload_json=dumps({"tool_key": key, "enabled": want_enabled,
"status": status, "reason": reason}),
)
return fail(
"PBL.APPROVAL.REQUIRED",
"tool_registry_change requires an approved human approval (fail-closed)",
tool_key=key, approval_type="tool_registry_change",
approval_id=created.get("approval_id"), approval_status="pending",
need_approval=True,
)
existed = get_store().query_one(
"SELECT * FROM pbl_tool_registry WHERE tenant_id = ? AND tool_key = ?", (tenant, key))
fields = {}
if tool_name is not None:
fields["tool_name"] = tool_name
if category is not None:
fields["category"] = category
if want_enabled is not None:
fields["status"] = "enabled" if want_enabled else "disabled"
if write_class is not None:
fields["write_class"] = 1 if _as_bool(write_class) else 0
if audit_append is not None:
fields["audit_append"] = 1 if _as_bool(audit_append) else 0
if required_perm is not None:
fields["required_perm"] = required_perm
if approval_type is not None:
fields["approval_type"] = approval_type or None
if params_schema is not None:
fields["params_schema"] = dumps(params_schema) if not isinstance(params_schema, str) else params_schema
if risk_level is not None:
fields["risk_level"] = risk_level
if reason is not None:
fields["reason"] = reason
if sort_no is not None:
fields["sort_no"] = _as_int(sort_no, 0)
fields["updated_at"] = now_str()
fields["updated_by"] = operator or "system"
fields["version"] = _as_int((existed or {}).get("version"), 1) + 1
row = get_store().upsert_by("pbl_tool_registry", {"tenant_id": tenant, "tool_key": key}, fields)
_write_trace_row(
tenant, trace_id=new_id("trc"), run_id=None, step_no=0, stage="tool_registry_change",
who=operator or "system", what="registry save tool_key=%s" % key,
why="注册表变更经人工审批 %s" % approval_row.get("approval_id"),
how="pbl_tool_registry_save", result="saved",
evidence_ref="pbl_approval:%s" % approval_row.get("approval_id"),
tool_key=key, decision="ALLOW",
)
return ok(tool=_tool_row_to_dict(row), item=_tool_row_to_dict(row), data=_tool_row_to_dict(row),
tool_key=key, tenant_id=tenant, saved=True,
approval_id=approval_row.get("approval_id"),
version=_as_int(row.get("version"), 1))
# ---------------------------------------------------------------------------
# 轨迹write / listappend-only
# ---------------------------------------------------------------------------
def _write_trace_row(tenant, trace_id, run_id, step_no, stage, who, what, why, how,
result, evidence_ref, tool_key=None, decision=None, occurred_at=None):
return get_store().insert(
"pbl_agent_trace",
{
"tenant_id": tenant, "trace_id": trace_id, "run_id": run_id,
"step_no": _as_int(step_no, 0), "stage": stage or "run",
"who": who or "system", "occurred_at": occurred_at or now_str(), "what": what,
"why": why, "how": how, "result": result, "evidence_ref": evidence_ref,
"tool_key": tool_key, "decision": decision, "created_at": now_str(),
},
)
def _pbl_agent_trace_write(tenant_id=None, trace_id=None, run_id=None, step_no=0, stage=None,
who=None, occurred_at=None, what=None, why=None, how=None,
result=None, evidence_ref=None, tool_key=None, decision=None,
elements=None, **_ignored) -> Result:
"""写一条 Agent 执行轨迹(第 28 章 7 要素who/occurred_at/what/why/how/result/evidence_ref"""
tenant = _tenant_or_raise(tenant_id)
if isinstance(elements, dict):
what = what if what is not None else elements.get("what")
why = why if why is not None else elements.get("why")
how = how if how is not None else elements.get("how")
result = result if result is not None else elements.get("result")
evidence_ref = evidence_ref if evidence_ref is not None else elements.get("evidence_ref")
who = who if who is not None else elements.get("who")
values = {
"who": who,
"occurred_at": occurred_at or now_str(),
"what": what,
"why": why,
"how": how,
"result": result,
"evidence_ref": evidence_ref,
}
missing = [name for name in TRACE_ELEMENTS
if values.get(name) in (None, "")]
if missing:
return fail("PBL.TRACE.ELEMENTS_REQUIRED",
"trace elements missing: %s (fail-closed)" % ",".join(missing),
missing=missing)
row = _write_trace_row(
tenant, trace_id=trace_id or new_id("trc"), run_id=run_id, step_no=step_no,
stage=stage, who=values["who"], what=values["what"], why=values["why"],
how=values["how"], result=values["result"], evidence_ref=values["evidence_ref"],
tool_key=tool_key, decision=decision, occurred_at=values["occurred_at"],
)
# occurred_at 为调用方给定的业务时间;留痕表 append-only只随新记录写入绝不 UPDATE
return ok(trace_id=row["trace_id"], id=row["id"], data=row, item=row,
tenant_id=tenant, written=True, elements=list(TRACE_ELEMENTS))
def _pbl_agent_trace_list(tenant_id=None, trace_id=None, run_id=None, agent_code=None,
stage=None, limit=100, page=1, page_size=None, **_ignored) -> Result:
tenant = _tenant_or_raise(tenant_id)
where = ["tenant_id = ?"]
args = [tenant]
if trace_id:
where.append("trace_id = ?")
args.append(str(trace_id))
if run_id:
where.append("run_id = ?")
args.append(str(run_id))
if stage:
where.append("stage = ?")
args.append(str(stage))
if agent_code:
where.append("who = ?")
args.append(str(agent_code))
where_sql = " AND ".join(where)
size = min(1000, max(1, _as_int(page_size or limit, 100)))
offset = (max(1, _as_int(page, 1)) - 1) * size
rows = get_store().query(
"SELECT * FROM pbl_agent_trace WHERE %s ORDER BY created_at DESC, step_no DESC LIMIT ? OFFSET ?"
% where_sql, args + [size, offset])
total_row = get_store().query_one(
"SELECT COUNT(1) AS cnt FROM pbl_agent_trace WHERE %s" % where_sql, args)
return ok(items=rows, data=rows, list=rows, total=_as_int((total_row or {}).get("cnt"), 0),
tenant_id=tenant, page=_as_int(page, 1), page_size=size)
# ---------------------------------------------------------------------------
# fail-closed 8 步裁决
# ---------------------------------------------------------------------------
def _validate_params(schema, params: dict):
"""极简 JSON-Schema 子集校验type/required/properties。返回 (ok, msg)。"""
schema = schema or {"type": "object"}
if not isinstance(schema, dict):
return True, ""
expected = str(schema.get("type") or "object").lower()
if expected == "object" and not isinstance(params, dict):
return False, "params must be object"
required = schema.get("required") or []
if isinstance(required, (list, tuple)):
for name in required:
if name not in params or params[name] in (None, ""):
return False, "missing required param: %s" % name
props = schema.get("properties") or {}
if isinstance(props, dict):
for name, spec in props.items():
if name not in params or not isinstance(spec, dict):
continue
want = str(spec.get("type") or "").lower()
value = params[name]
if want == "string" and not isinstance(value, str):
return False, "param %s must be string" % name
if want in ("int", "integer") and not isinstance(value, int):
return False, "param %s must be integer" % name
if want in ("number", "float") and not isinstance(value, (int, float)):
return False, "param %s must be number" % name
if want in ("bool", "boolean") and not isinstance(value, bool):
return False, "param %s must be boolean" % name
return True, ""
def _pbl_tool_adjudicate(tenant_id=None, agent_code=None, tool_key=None, params_json=None,
params=None, trace_id=None, run_id=None, approval_id=None,
dry_run=False, **_ignored) -> Result:
"""服务端 fail-closed 8 步裁决。默认 DENY任一步不过立即 DENY 并留痕。"""
started = now_ms()
tenant = _tenant_or_raise(tenant_id)
seed_registry(tenant)
store = get_store()
agent_code = (agent_code or "").strip()
tool_key = (tool_key or "").strip()
raw_params = params_json if params_json is not None else params
if isinstance(raw_params, str):
parsed = loads(raw_params, None)
if parsed is None and raw_params.strip():
parsed = None
params_invalid = "params_json is not valid JSON"
else:
params_invalid = None
elif isinstance(raw_params, dict):
parsed = dict(raw_params)
params_invalid = None
elif raw_params in (None, ""):
parsed = {}
params_invalid = None
else:
parsed = {}
params_invalid = "params_json must be object or JSON string"
trace_id = trace_id or new_id("trc")
steps_done = []
def deny(step_no, step_key, reason_code, message, **extra):
latency = now_ms() - started
out = fail(reason_code, message, allowed=False, decision="DENY", denied=True,
agent_code=agent_code, tool_key=tool_key, tenant_id=tenant,
trace_id=trace_id, denied_at_step=step_key, denied_step_no=step_no,
reason_code=reason_code, steps=steps_done, latency_ms=latency,
need_approval=reason_code == "PBL.APPROVAL.REQUIRED")
out.update(extra)
if not dry_run:
try:
store.insert("pbl_agent_tool_call", {
"tenant_id": tenant, "trace_id": trace_id,
"call_no": _as_int(store.query_one(
"SELECT COUNT(1) AS c FROM pbl_agent_tool_call WHERE tenant_id = ? AND trace_id = ?",
(tenant, trace_id)) or {}, 0) + 1,
"agent_code": agent_code or "unknown", "tool_key": tool_key or "unknown",
"params_json": dumps(parsed), "decision": "DENY",
"denied_at_step": step_key, "reason_code": reason_code,
"approval_id": extra.get("approval_id"), "latency_ms": latency,
"created_at": now_str(),
})
_write_trace_row(
tenant, trace_id=trace_id, run_id=run_id, step_no=step_no, stage="adjudicate",
who=agent_code or "unknown", what="tool_adjudicate tool_key=%s" % tool_key,
why=message, how="fail-closed step %s" % step_key, result="DENY",
evidence_ref="pbl_agent_tool_call:%s" % trace_id, tool_key=tool_key,
decision="DENY")
except PblContractError:
raise
return out
def allow(step_no, approval_row=None, **extra):
latency = now_ms() - started
out = ok(allowed=True, decision="ALLOW", denied=False, agent_code=agent_code,
tool_key=tool_key, tenant_id=tenant, trace_id=trace_id, steps=steps_done,
latency_ms=latency, reason_code=None)
out.update(extra)
if not dry_run:
store.insert("pbl_agent_tool_call", {
"tenant_id": tenant, "trace_id": trace_id,
"call_no": _as_int(store.query_one(
"SELECT COUNT(1) AS c FROM pbl_agent_tool_call WHERE tenant_id = ? AND trace_id = ?",
(tenant, trace_id)) or {}, 0) + 1,
"agent_code": agent_code or "unknown", "tool_key": tool_key or "unknown",
"params_json": dumps(parsed), "decision": "ALLOW", "denied_at_step": None,
"reason_code": None,
"approval_id": (approval_row or {}).get("approval_id"),
"latency_ms": latency, "created_at": now_str(),
})
_write_trace_row(
tenant, trace_id=trace_id, run_id=run_id, step_no=step_no, stage="adjudicate",
who=agent_code or "unknown", what="tool_adjudicate tool_key=%s" % tool_key,
why="8 步裁决全部通过", how="fail-closed adjudication S1..S8", result="ALLOW",
evidence_ref="pbl_agent_tool_call:%s" % trace_id, tool_key=tool_key,
decision="ALLOW")
return out
# S1 租户上下文
steps_done.append({"step": "S1", "key": "tenant_context", "pass": True})
# S2 Agent 已注册且 active
if not agent_code:
return deny(2, "agent_registered", "PBL.AGENT.CODE_REQUIRED", "agent_code is required")
agent = store.query_one(
"SELECT * FROM pbl_agent_def WHERE tenant_id = ? AND agent_code = ?", (tenant, agent_code))
if not agent:
return deny(2, "agent_registered", "PBL.AGENT.NOT_REGISTERED",
"agent %s not registered (fail-closed)" % agent_code, agent_code=agent_code)
if str(agent.get("status") or "").lower() != "active":
return deny(2, "agent_registered", "PBL.AGENT.NOT_ACTIVE",
"agent %s is not active" % agent_code, agent_status=agent.get("status"))
steps_done.append({"step": "S2", "key": "agent_registered", "pass": True})
# S3 Critic 零写
tool_row = store.query_one(
"SELECT * FROM pbl_tool_registry WHERE tenant_id = ? AND tool_key = ?", (tenant, tool_key))
is_write = bool(tool_row and _as_int(tool_row.get("write_class"), 0) == 1)
can_write = _as_int(agent.get("can_write"), 0) == 1
if is_write and not can_write:
return deny(3, "agent_write_scope", "PBL.AGENT.WRITE_DENIED",
"agent %s has zero write permission (Critic), tool %s denied"
% (agent_code, tool_key), write_class=1, can_write=False)
steps_done.append({"step": "S3", "key": "agent_write_scope", "pass": True})
# S4 工具已登记
if not tool_key:
return deny(4, "tool_registered", "PBL.TOOL.KEY_REQUIRED", "tool_key is required")
if not tool_row:
return deny(4, "tool_registered", "PBL.TOOL.NOT_REGISTERED",
"tool %s not in registry (fail-closed default DENY)" % tool_key)
steps_done.append({"step": "S4", "key": "tool_registered", "pass": True})
# S5 工具启用
if str(tool_row.get("status") or "").lower() != "enabled":
return deny(5, "tool_enabled", "PBL.TOOL.DISABLED",
"tool %s is disabled: %s" % (tool_key, tool_row.get("reason") or "by design"),
tool_status=tool_row.get("status"))
steps_done.append({"step": "S5", "key": "tool_enabled", "pass": True})
# S6 权限
required_perm = tool_row.get("required_perm")
agent_perms = loads(agent.get("perms"), []) or []
if required_perm and required_perm not in agent_perms:
return deny(6, "permission_granted", "PBL.PERM.DENIED",
"agent %s lacks perm %s for tool %s" % (agent_code, required_perm, tool_key),
required_perm=required_perm)
steps_done.append({"step": "S6", "key": "permission_granted", "pass": True})
# S7 参数校验
if params_invalid:
return deny(7, "params_valid", "PBL.PARAMS.INVALID", params_invalid)
schema = loads(tool_row.get("params_schema"), {"type": "object"})
valid, message = _validate_params(schema, parsed)
if not valid:
return deny(7, "params_valid", "PBL.PARAMS.INVALID", message)
steps_done.append({"step": "S7", "key": "params_valid", "pass": True})
# S8 四类强制人工审批
approval_type = tool_row.get("approval_type")
approval_row = None
if approval_type or approval_id:
if approval_id:
approval_row = store.query_one(
"SELECT * FROM pbl_approval WHERE tenant_id = ? AND approval_id = ?",
(tenant, str(approval_id)))
else:
approval_row = store.query_one(
"SELECT * FROM pbl_approval WHERE tenant_id = ? AND approval_type = ? "
"AND tool_key = ? AND status = 'approved' ORDER BY decided_at DESC LIMIT 1",
(tenant, approval_type, tool_key))
if not approval_row or str(approval_row.get("status")) != "approved":
created = _pbl_approval_create(
tenant_id=tenant, approval_type=approval_type or "blueprint_approve",
title="工具 %s 需人工审批" % tool_key, requested_by=agent_code,
agent_code=agent_code, tool_key=tool_key, params_json=dumps(parsed),
)
return deny(8, "approval_granted", "PBL.APPROVAL.REQUIRED",
"tool %s requires human approval type=%s (no bypass)"
% (tool_key, approval_type),
approval_type=approval_type, approval_id=created.get("approval_id"),
approval_status="pending")
steps_done.append({"step": "S8", "key": "approval_granted", "pass": True})
return allow(8, approval_row=approval_row,
approval_id=(approval_row or {}).get("approval_id"),
approval_type=approval_type, write_class=1 if is_write else 0)
# ---------------------------------------------------------------------------
# 审批create / decide / list
# ---------------------------------------------------------------------------
def _pbl_approval_create(tenant_id=None, approval_type=None, title=None, payload_json=None,
requested_by=None, agent_code=None, tool_key=None, params_json=None,
expires_in_hours=72, **_ignored) -> Result:
"""创建人工审批单14.2 四类。approval_type 非法 → 拒绝fail-closed"""
tenant = _tenant_or_raise(tenant_id)
kind = (approval_type or "").strip()
if not kind:
return fail("PBL.APPROVAL.TYPE_REQUIRED", "approval_type is required")
if kind not in MANDATORY_APPROVAL_TYPES:
return fail("PBL.APPROVAL.TYPE_INVALID",
"approval_type %s not in mandatory types %s"
% (kind, ",".join(MANDATORY_APPROVAL_TYPES)),
approval_type=kind, allowed_types=list(MANDATORY_APPROVAL_TYPES))
payload = payload_json if payload_json is not None else params_json
approval_id = new_id("apr")
expires_at = time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() + _as_int(expires_in_hours, 72) * 3600))
row = get_store().insert(
"pbl_approval",
{
"tenant_id": tenant, "approval_id": approval_id, "approval_type": kind,
"title": title or "%s 审批" % kind,
"payload_json": dumps(payload) if not isinstance(payload, str) else payload,
"status": "pending", "requested_by": requested_by or agent_code or "system",
"agent_code": agent_code, "tool_key": tool_key, "expires_at": expires_at,
"created_at": now_str(), "updated_at": now_str(),
},
)
return ok(approval_id=approval_id, id=row["id"], data=row, item=row,
approval_type=kind, status="pending", tenant_id=tenant, expires_at=expires_at)
def _pbl_approval_decide(tenant_id=None, approval_id=None, decision=None, action=None,
decided_by=None, comment=None, decision_comment=None, **_ignored) -> Result:
"""人工审批裁决approve / reject。只有真人decided_by 非 agent可裁决。"""
tenant = _tenant_or_raise(tenant_id)
verdict = (decision or action or "").strip().lower()
if verdict in ("approved", "pass", "yes", "allow"):
verdict = "approve"
if verdict in ("rejected", "deny", "denied", "no"):
verdict = "reject"
if verdict not in ("approve", "reject"):
return fail("PBL.APPROVAL.DECISION_INVALID",
"decision must be approve|reject, got %r" % (decision or action))
if not approval_id:
return fail("PBL.APPROVAL.ID_REQUIRED", "approval_id is required")
decider = (decided_by or "").strip()
if not decider:
return fail("PBL.APPROVAL.DECIDER_REQUIRED", "decided_by is required (human only)")
if decider in ("designer", "critic", "agent", "system"):
return fail("PBL.APPROVAL.NO_BYPASS",
"approval cannot be decided by agent %s (no bypass path)" % decider)
row = get_store().query_one(
"SELECT * FROM pbl_approval WHERE tenant_id = ? AND approval_id = ?",
(tenant, str(approval_id)))
if not row:
return fail(ERR_NOT_FOUND, "approval %s not found" % approval_id, approval_id=approval_id)
if str(row.get("status")) != "pending":
return fail("PBL.APPROVAL.ALREADY_DECIDED",
"approval %s already %s" % (approval_id, row.get("status")),
status=row.get("status"))
new_status = "approved" if verdict == "approve" else "rejected"
get_store().execute(
"UPDATE pbl_approval SET status = ?, decided_by = ?, decided_at = ?, "
"decision_comment = ?, updated_at = ? WHERE id = ?",
(new_status, decider, now_str(), (comment or decision_comment or ""), now_str(), row["id"]),
)
updated = get_store().query_one(
"SELECT * FROM pbl_approval WHERE id = ?", (row["id"],))
return ok(approval_id=str(approval_id), status=new_status, decision=verdict,
data=updated, item=updated, tenant_id=tenant, decided_by=decider)
def _pbl_approval_list(tenant_id=None, status=None, approval_type=None, agent_code=None,
tool_key=None, limit=100, page=1, page_size=None, **_ignored) -> Result:
tenant = _tenant_or_raise(tenant_id)
where = ["tenant_id = ?"]
args = [tenant]
if status:
where.append("status = ?")
args.append(str(status).strip().lower())
if approval_type:
where.append("approval_type = ?")
args.append(str(approval_type))
if agent_code:
where.append("agent_code = ?")
args.append(str(agent_code))
if tool_key:
where.append("tool_key = ?")
args.append(str(tool_key))
where_sql = " AND ".join(where)
size = min(500, max(1, _as_int(page_size or limit, 100)))
offset = (max(1, _as_int(page, 1)) - 1) * size
rows = get_store().query(
"SELECT * FROM pbl_approval WHERE %s ORDER BY created_at DESC LIMIT ? OFFSET ?"
% where_sql, args + [size, offset])
for row in rows:
row["payload"] = loads(row.get("payload_json"), {})
total_row = get_store().query_one(
"SELECT COUNT(1) AS cnt FROM pbl_approval WHERE %s" % where_sql, args)
return ok(items=rows, data=rows, list=rows, total=_as_int((total_row or {}).get("cnt"), 0),
tenant_id=tenant, page=_as_int(page, 1), page_size=size)
# ---------------------------------------------------------------------------
# Designer / Critic 运行时
# ---------------------------------------------------------------------------
def _agent_run(agent_code, tenant_id=None, run_id=None, task=None, prompt=None, blueprint_id=None,
tools=None, model=None, trace_id=None, **_ignored) -> Result:
tenant = _tenant_or_raise(tenant_id)
seed_registry(tenant)
store = get_store()
agent = store.query_one(
"SELECT * FROM pbl_agent_def WHERE tenant_id = ? AND agent_code = ?", (tenant, agent_code))
if not agent:
return fail("PBL.AGENT.NOT_REGISTERED", "agent %s not registered" % agent_code,
agent_code=agent_code, allowed=False)
if str(agent.get("status") or "").lower() != "active":
return fail("PBL.AGENT.NOT_ACTIVE", "agent %s not active" % agent_code,
allowed=False, agent_status=agent.get("status"))
run_id = run_id or new_id("run")
trace_id = trace_id or new_id("trc")
can_write = _as_int(agent.get("can_write"), 0) == 1
requested = tools if isinstance(tools, (list, tuple)) else loads(tools, []) or []
if agent_code == "critic" and requested:
# Critic 零写:任何写类工具请求在运行入口即被拒绝(不等 S3
write_requested = []
for key in requested:
row = store.query_one(
"SELECT write_class FROM pbl_tool_registry WHERE tenant_id = ? AND tool_key = ?",
(tenant, str(key)))
if row and _as_int(row.get("write_class"), 0) == 1:
write_requested.append(str(key))
if write_requested:
return fail("PBL.AGENT.WRITE_DENIED",
"critic has zero write permission, denied tools: %s"
% ",".join(write_requested),
allowed=False, agent_code=agent_code, run_id=run_id,
denied_tools=write_requested, can_write=False)
prompt_text = prompt or task or ""
llm = _invoke_llm(tenant, agent_code, run_id,
"designer_run" if agent_code == "designer" else "critic_run",
"%s\n%s" % (agent.get("system_prompt") or "", prompt_text), model=model)
adjudications = []
for key in requested:
verdict = _pbl_tool_adjudicate(
tenant_id=tenant, agent_code=agent_code, tool_key=str(key),
params_json=dumps({"run_id": run_id, "blueprint_id": blueprint_id}),
run_id=run_id, trace_id=None)
adjudications.append({"tool_key": str(key), "decision": verdict.get("decision"),
"reason_code": verdict.get("reason_code"),
"denied_at_step": verdict.get("denied_at_step")})
_write_trace_row(
tenant, trace_id=trace_id, run_id=run_id, step_no=1, stage="%s_run" % agent_code,
who=agent_code, what="run task: %s" % (str(prompt_text)[:200]),
why=agent.get("system_prompt") or "", how="llm=%s attempt=%s"
% (llm.get("text", "")[:12], llm.get("attempt_no")),
result="fallback" if llm.get("fallback_used") else "ok",
evidence_ref="pbl_llm_call_log:%s" % run_id, tool_key=None,
decision="ALLOW" if can_write else "READ_ONLY")
return ok(run_id=run_id, trace_id=trace_id, agent_code=agent_code, tenant_id=tenant,
can_write=can_write, write_scope=agent.get("write_scope"),
output=llm.get("text"), text=llm.get("text"), data={"output": llm.get("text")},
llm=llm, adjudications=adjudications,
tool_calls=len(adjudications),
allowed=can_write,
fallback_used=bool(llm.get("fallback_used")),
status="fallback" if llm.get("fallback_used") else "ok")
def _pbl_agent_designer_run(**kwargs) -> Result:
return _agent_run("designer", **kwargs)
def _pbl_agent_critic_run(**kwargs) -> Result:
return _agent_run("critic", **kwargs)
# ---------------------------------------------------------------------------
# 契约对象 api()
# ---------------------------------------------------------------------------
_IMPL = {
"pbl_agent_designer_run": _pbl_agent_designer_run,
"pbl_agent_critic_run": _pbl_agent_critic_run,
"pbl_agent_trace_write": _pbl_agent_trace_write,
"pbl_agent_trace_list": _pbl_agent_trace_list,
"pbl_tool_registry_list": _pbl_tool_registry_list,
"pbl_tool_registry_save": _pbl_tool_registry_save,
"pbl_tool_adjudicate": _pbl_tool_adjudicate,
"pbl_approval_create": _pbl_approval_create,
"pbl_approval_decide": _pbl_approval_decide,
"pbl_approval_list": _pbl_approval_list,
}
def _wrapped(name):
impl = _IMPL[name]
def contract_fn(**kwargs):
return _run(impl, **kwargs)
contract_fn.__name__ = name
contract_fn.__qualname__ = name
contract_fn.__doc__ = (impl.__doc__ or "").strip().splitlines()[0] if (impl.__doc__ or "").strip() else name
contract_fn.contract_name = name
return contract_fn
class _ContractApi(dict):
"""契约对象:既可 ``api.pbl_tool_adjudicate(...)``,也可 ``api['pbl_tool_adjudicate'](...)``。"""
def __getattr__(self, item):
try:
return self[item]
except KeyError:
raise AttributeError("pbl_agent_runtime contract has no function %r" % item)
def names(self):
return list(CONTRACT_FUNCTIONS)
def build_api() -> _ContractApi:
api_obj = _ContractApi()
for name in CONTRACT_FUNCTIONS:
api_obj[name] = _wrapped(name)
# 附加运维/自检能力(非契约端点,不进 load_path
api_obj["seed_registry"] = lambda **kw: _run(seed_registry, **kw)
api_obj["log_llm_call"] = lambda **kw: _run(log_llm_call, **kw)
api_obj["self_check"] = lambda **kw: _run(self_check, **kw)
api_obj["CONTRACT_FUNCTIONS"] = list(CONTRACT_FUNCTIONS)
api_obj["ADJUDICATION_STEPS"] = list(ADJUDICATION_STEPS)
api_obj["MANDATORY_APPROVAL_TYPES"] = list(MANDATORY_APPROVAL_TYPES)
api_obj["TRACE_ELEMENTS"] = list(TRACE_ELEMENTS)
api_obj["APPEND_ONLY_TABLES"] = list(APPEND_ONLY_TABLES)
return api_obj
# ---------------------------------------------------------------------------
# 自检:挂载即校验,不过抛 RuntimeErrorfail-closed
# ---------------------------------------------------------------------------
def self_check(tenant_id=None, strict=True) -> dict:
"""挂载自检:契约齐全 + seed 幂等 + fail-closed 语义 + append-only 守卫。"""
problems = []
tenant = resolve_tenant(tenant_id) or "0"
api_obj = build_api()
for name in CONTRACT_FUNCTIONS:
if not callable(api_obj.get(name)):
problems.append("contract function missing: %s" % name)
try:
seed = seed_registry(tenant)
if seed["enabled"] != 13 or seed["disabled"] != 9:
problems.append("tool seed count wrong: %s" % seed)
except Exception as exc: # noqa: BLE001
problems.append("seed_registry failed: %s" % exc)
listing = _run(_pbl_tool_registry_list, tenant_id=tenant)
if not listing.get("success"):
problems.append("pbl_tool_registry_list failed: %s" % listing.get("error_msg"))
else:
keys = {item["tool_key"]: item for item in listing.get("items", [])}
if "pbl.publish" not in keys:
problems.append("pbl.publish missing from registry")
elif keys["pbl.publish"].get("status") != "disabled":
problems.append("pbl.publish must be disabled")
enabled = [k for k, v in keys.items() if v.get("enabled")]
if len(enabled) != 13:
problems.append("enabled tool count=%d, expect 13" % len(enabled))
critic_write = _run(_pbl_tool_adjudicate, tenant_id=tenant, agent_code="critic",
tool_key="blueprint.write", params_json="{}")
if critic_write.get("allowed") is not False:
problems.append("critic write tool was not denied (zero-write violated)")
elif critic_write.get("denied_at_step") != "agent_write_scope":
problems.append("critic deny step=%s, expect agent_write_scope"
% critic_write.get("denied_at_step"))
publish = _run(_pbl_tool_adjudicate, tenant_id=tenant, agent_code="designer",
tool_key="pbl.publish", params_json="{}")
if publish.get("allowed") is not False:
problems.append("pbl.publish was not denied")
unknown = _run(_pbl_tool_adjudicate, tenant_id=tenant, agent_code="designer",
tool_key="no.such.tool", params_json="{}")
if unknown.get("allowed") is not False or unknown.get("decision") != "DENY":
problems.append("unknown tool default decision is not DENY")
no_tenant = _run(_pbl_tool_adjudicate, tenant_id="", agent_code="designer",
tool_key="blueprint.read", params_json="{}")
if no_tenant.get("success") is not False:
problems.append("missing tenant_id was not rejected")
store = get_store()
try:
store.execute("DELETE FROM pbl_agent_trace WHERE 1 = 0")
problems.append("append-only guard failed on pbl_agent_trace")
except PblContractError as exc:
if exc.code != ERR_APPEND_ONLY:
problems.append("append-only guard wrong code: %s" % exc.code)
approval_bad = _run(_pbl_approval_create, tenant_id=tenant, approval_type="not_a_type")
if approval_bad.get("success") is not False:
problems.append("invalid approval_type was accepted")
result = {
"success": not problems,
"module": "pbl_agent_runtime",
"tenant_id": tenant,
"contract_functions": list(CONTRACT_FUNCTIONS),
"contract_count": len(CONTRACT_FUNCTIONS),
"tables": 6,
"append_only_tables": list(APPEND_ONLY_TABLES),
"adjudication_steps": [s[0] for s in ADJUDICATION_STEPS],
"mandatory_approval_types": list(MANDATORY_APPROVAL_TYPES),
"problems": problems,
}
if problems and strict:
raise RuntimeError("pbl_agent_runtime self_check failed (fail-closed): %s"
% "; ".join(problems))
return Result(result)