feat: tool approval declarations (v9)
Agent definitions gain tool_approvals: known tool -> ask/auto/deny. Unknown tool/policy is a 400, and required+deny is a 400 contradiction. The declaration is persisted (tasks.agent_tool_ approvals, PG + migrate), echoed in GET and the creation response (via to_summary), and resolved for registry definitions -- fixing a latent drop where resolve_registry_agent re-parsed without new fields. At dispatch the worker audits the declaration and fails fast on a defensive required+deny re-check (v3 tools-gate shape, runner never called); goal/coordinator tasks reject the field at creation like instructions. effective_tool_approvals maps a declaration + host global approval to the actual host answer, with ask_honoured always False: host answers elicitation with one global switch and carries no tool identity, and the approvals queue is record-only with no runtime ask channel, so per-tool interception and credential scope are explicitly NOT claimed.
This commit is contained in:
1 parent
cd33a929ca
commit
a51919f24b
6 files changed
+441
-6
No files matched your search
@@ -370,6 +370,20 @@ def _agent_tools_from_col(raw):
|
||||
return list(parsed) if isinstance(parsed, list) else []
|
||||
|
||||
|
||||
def _agent_tool_approvals_from_col(raw):
|
||||
"""agent_tool_approvals 列的容错解析:JSON 对象,非对象/
|
||||
脏数据为 {}(= 未声明),不炸认领。"""
|
||||
if raw is None:
|
||||
return {}
|
||||
if isinstance(raw, dict):
|
||||
return dict(raw)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return {}
|
||||
return dict(parsed) if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def _task_from_row(row):
|
||||
depends_on = row["depends_on"]
|
||||
try:
|
||||
@@ -441,6 +455,10 @@ def _task_from_row(row):
|
||||
# v3:agent_tools 存 JSON 数组;脏数据/老行缺列时
|
||||
# 按 [] 计(= 未声明必需工具),不炸认领。
|
||||
"agent_tools": _agent_tools_from_col(_col("agent_tools")),
|
||||
# v9:agent_tool_approvals 存 JSON 对象;脏数据/老行
|
||||
# 缺列时按 {} 计(= 未声明),不炸认领。
|
||||
"agent_tool_approvals": _agent_tool_approvals_from_col(
|
||||
_col("agent_tool_approvals")),
|
||||
}
|
||||
|
||||
|
||||
@@ -6101,6 +6119,51 @@ class Worker:
|
||||
version=task.get("agent_version"),
|
||||
instructions=task["agent_instructions"]),
|
||||
task.get("prompt") or "")
|
||||
# agent 工具审批声明(v9):派发前审计声明本身,并
|
||||
# 防御性重查 required+deny 矛盾(创建时已拒绝,防
|
||||
# v9 之前或 API 外写入的行漏网)。审计只记声明,不
|
||||
# 宣称运行时按工具拦截:host 的 elicitation 回答是
|
||||
# 全局开关(auto 接受、其余拒绝),没有按工具分流,
|
||||
# ask 在今天没有询问通道,见 agent_def.py 的 v9 边界。
|
||||
_tool_approvals = task.get("agent_tool_approvals") or {}
|
||||
if _tool_approvals:
|
||||
_denied_required = [
|
||||
t for t in (task.get("agent_tools") or [])
|
||||
if _tool_approvals.get(t) == "deny"]
|
||||
if _denied_required:
|
||||
_appr_error = (
|
||||
"agent 工具审批声明矛盾:必需工具 %s 被声明为"
|
||||
" deny,任务无法使用其必需工具"
|
||||
% ", ".join(_denied_required))
|
||||
self._set_status(task_id, "failed")
|
||||
self._write_execution_result(
|
||||
task_id, error_text=_appr_error,
|
||||
actual_model=self.model,
|
||||
started_at=started_at, finished_at=time.time(),
|
||||
only_if_status="failed")
|
||||
self._audit(task["project_id"],
|
||||
"worker_tool_approval_gate_rejected",
|
||||
"%s: %s" % (task_id, _appr_error))
|
||||
log_event(self.logger,
|
||||
"worker_tool_approval_gate_rejected",
|
||||
task_id=task_id, project_id=task["project_id"],
|
||||
status="failed")
|
||||
self._running_clear(task_id)
|
||||
return {"dispatched": True, "task_id": task_id,
|
||||
"host_name": self.executor._host_name(task)
|
||||
if hasattr(self.executor, "_host_name")
|
||||
else task_id,
|
||||
"status": "failed",
|
||||
"executor": self._executor_name(),
|
||||
"stdout": "", "error": _appr_error, "events": [],
|
||||
"cost_record_id": None}
|
||||
self._audit(task["project_id"],
|
||||
"worker_tool_approvals_declared",
|
||||
"%s: %s (per-tool interception is not enforced"
|
||||
" at runtime; host answers elicitation globally)"
|
||||
% (task_id, json.dumps(_tool_approvals,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True)))
|
||||
# agent 必需工具门(v3):协调员父任务已在前面返回,
|
||||
# 这里只剩真正要跑模型的任务(普通/goal/fork 首轮都
|
||||
# 走后面的尝试循环,门放在循环前,一次定罪不重试)。
|
||||
|
||||
Reference in new issue
Block a user