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
+35
-3
@@ -1247,6 +1247,8 @@ def migrate_tasks_table(conn):
|
||||
"agent_instructions": "TEXT",
|
||||
# agent 定义 v3:必需工具 JSON 数组(NULL = 未声明)。
|
||||
"agent_tools": "TEXT",
|
||||
# agent 定义 v9:工具审批声明 JSON 对象(NULL = 未声明)。
|
||||
"agent_tool_approvals": "TEXT",
|
||||
}
|
||||
for name, coltype in new_cols.items():
|
||||
if name not in cols:
|
||||
@@ -1807,6 +1809,21 @@ def _agent_tools_from_row(row):
|
||||
return list(parsed) if isinstance(parsed, list) else None
|
||||
|
||||
|
||||
def _agent_tool_approvals_from_row(row):
|
||||
"""agent_tool_approvals 列的容错解析:JSON 对象,非对象/脏
|
||||
数据为 None(NULL = 未声明,与 v3 tools 同款语义)。"""
|
||||
raw = _row_get(row, "agent_tool_approvals")
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, dict):
|
||||
return dict(raw)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return dict(parsed) if isinstance(parsed, dict) else None
|
||||
|
||||
|
||||
def _task_to_dict(row, attachments=None, total_tokens=None):
|
||||
depends_on = row["depends_on"]
|
||||
try:
|
||||
@@ -1868,6 +1885,9 @@ def _task_to_dict(row, attachments=None, total_tokens=None):
|
||||
# v3:agent_tools 存 JSON 数组;脏数据/老行缺列时
|
||||
# 按 None 计,不炸列表接口。
|
||||
"agent_tools": _agent_tools_from_row(row),
|
||||
# v9:agent_tool_approvals 存 JSON 对象;脏数据/老行
|
||||
# 缺列时按 None 计(= 未声明)。
|
||||
"agent_tool_approvals": _agent_tool_approvals_from_row(row),
|
||||
"paused_by_user": _row_get(row, "paused_by_user") or 0,
|
||||
"interrupted_reason": _row_get(row, "interrupted_reason"),
|
||||
"auto_resume_count": _row_get(row, "auto_resume_count") or 0,
|
||||
@@ -4766,12 +4786,23 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
"error": "agent instructions cannot be used with"
|
||||
" goal/coordinator tasks"})
|
||||
return
|
||||
# v9:工具审批声明与指令同款限制——goal 循环与协调员
|
||||
# 父任务不传声明,接受它就是静默丢弃,所以同样拒绝。
|
||||
if definition is not None and definition.tool_approvals:
|
||||
if goal_mode or verify_command or coord_parts:
|
||||
self._send_json(400, {
|
||||
"error": "agent tool_approvals cannot be used with"
|
||||
" goal/coordinator tasks"})
|
||||
return
|
||||
agent_name_val = definition.name if definition else None
|
||||
agent_version_val = definition.version if definition else None
|
||||
agent_instructions_val = (
|
||||
definition.instructions if definition else None)
|
||||
agent_tools_val = (
|
||||
json.dumps(definition.tools) if definition else None)
|
||||
agent_tool_approvals_val = (
|
||||
json.dumps(definition.tool_approvals)
|
||||
if definition and definition.tool_approvals else None)
|
||||
now = time.time()
|
||||
conn = self._db()
|
||||
try:
|
||||
@@ -4798,9 +4829,9 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
" parent_task_id, coordinator_part,"
|
||||
" child_model, child_effort,"
|
||||
" agent_name, agent_version, agent_instructions,"
|
||||
" agent_tools)"
|
||||
" agent_tools, agent_tool_approvals)"
|
||||
" VALUES(?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, NULL, ?,"
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
" ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, idempotency_key,
|
||||
json.dumps(depends_on), priority, user_id, now, now,
|
||||
executor, goal, verify_command, goal_max_rounds,
|
||||
@@ -4809,7 +4840,8 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
||||
json.dumps(coord_parts) if coord_parts else None,
|
||||
None, None, child_model, child_effort,
|
||||
agent_name_val, agent_version_val,
|
||||
agent_instructions_val, agent_tools_val),
|
||||
agent_instructions_val, agent_tools_val,
|
||||
agent_tool_approvals_val),
|
||||
)
|
||||
# 长任务不限时:API 字段 no_time_limit=1 时写标记,
|
||||
# worker 认领后按不限时跑(单轮 host 600 秒仍在);
|
||||
|
||||
Reference in new issue
Block a user