feat: agent tools as required tools with tenant tool gate
Agent definitions gain tools (websearch/imagegen/playwright, unknown tool is a 400), stored as agent_tools JSON and returned by GET /tasks. This is deliberately required-tools, not an allowlist: create_thread has no tool allowlist and tenant homes are per-project shared, so blocking other tools cannot be claimed. Instead the worker fails fast before any model call when a declared tool has no [mcp_servers.<tool>] section in the tenant codex-home config.toml (checked after _tenant_dirs seeding, so a section seeded earlier by another worker counts as present). The gate runs only for tenant-isolated Docker workers; local/cloud workers skip it, documented, not verified. Tests: test_agent_tools.py (8 tests).
This commit is contained in:
1 parent
1672ab9ff2
commit
fd37505c48
6 files changed
+383
-7
No files matched your search
@@ -136,6 +136,7 @@ from types import SimpleNamespace
|
||||
|
||||
from cloud.agent_def import AgentDefinition as _AgentDefinition
|
||||
from cloud.agent_def import format_instructions_header as _agent_header
|
||||
from cloud.agent_def import missing_required_tools as _missing_tools
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.coordinator import build_report as _coord_build_report
|
||||
from cloud.coordinator import DEFAULT_POLL_INTERVAL as _COORD_POLL_INTERVAL
|
||||
@@ -350,6 +351,19 @@ def _resolve_model(model=None, config_path=None):
|
||||
return DEFAULT_MODEL
|
||||
|
||||
|
||||
def _agent_tools_from_col(raw):
|
||||
"""agent_tools 列的容错解析:JSON 数组,非数组/脏数据为 []。"""
|
||||
if raw is None:
|
||||
return []
|
||||
if isinstance(raw, list):
|
||||
return list(raw)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return []
|
||||
return list(parsed) if isinstance(parsed, list) else []
|
||||
|
||||
|
||||
def _task_from_row(row):
|
||||
depends_on = row["depends_on"]
|
||||
try:
|
||||
@@ -418,6 +432,9 @@ def _task_from_row(row):
|
||||
"agent_name": _col("agent_name"),
|
||||
"agent_version": _col("agent_version"),
|
||||
"agent_instructions": _col("agent_instructions"),
|
||||
# v3:agent_tools 存 JSON 数组;脏数据/老行缺列时
|
||||
# 按 [] 计(= 未声明必需工具),不炸认领。
|
||||
"agent_tools": _agent_tools_from_col(_col("agent_tools")),
|
||||
}
|
||||
|
||||
|
||||
@@ -5535,6 +5552,44 @@ class Worker:
|
||||
self.usage_cost_usd += micros / 1000000.0
|
||||
return cost_id
|
||||
|
||||
def _missing_required_tools(self, task):
|
||||
"""agent 必需工具门(v3):返回缺失工具名列表。
|
||||
|
||||
只在租户隔离 Docker worker 上查:租户 codex-home 的
|
||||
config.toml 是实际可用性的记录(_tenant_dirs 已按
|
||||
worker 旗播种过,之前别的 worker 播过的段会保留,
|
||||
所以只看旗会误判)。返回 [] = 不缺或本 worker 不
|
||||
适用(非租户/非 Docker/未声明工具);返回非空 =
|
||||
声明了但租户 home 里没有对应 [mcp_servers.<tool>]
|
||||
段,run_one 应在模型调用前直接 failed,不静默跑
|
||||
一个注定缺工具的任务。租户目录准备失败时不由本
|
||||
方法定罪(返回 [],让 _docker_task_context 的
|
||||
租户准备失败路径处理),避免同一失败被两处各说一遍。
|
||||
"""
|
||||
tools = task.get("agent_tools") or []
|
||||
if not tools:
|
||||
return []
|
||||
if not self.tenant_isolation:
|
||||
return []
|
||||
if not isinstance(self.executor, DockerExecutor):
|
||||
return []
|
||||
try:
|
||||
home = self._tenant_dirs(
|
||||
task.get("project_id"))["codex_home"]
|
||||
with open(os.path.join(home, "config.toml"),
|
||||
encoding="utf-8") as fh:
|
||||
config_text = fh.read()
|
||||
except Exception as exc:
|
||||
# 不由本方法定罪,但绝不静默吞掉:记日志,真正
|
||||
# 的租户准备失败由 _docker_task_context 报错。
|
||||
try:
|
||||
self.logger.warning(
|
||||
"required tools check skipped: %s", exc)
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
return _missing_tools(tools, config_text)
|
||||
|
||||
def run_one(self, task=None):
|
||||
"""认领并执行一个任务,返回结果 dict(见模块 docstring)。
|
||||
|
||||
@@ -5885,6 +5940,35 @@ class Worker:
|
||||
version=task.get("agent_version"),
|
||||
instructions=task["agent_instructions"]),
|
||||
task.get("prompt") or "")
|
||||
# agent 必需工具门(v3):协调员父任务已在前面返回,
|
||||
# 这里只剩真正要跑模型的任务(普通/goal/fork 首轮都
|
||||
# 走后面的尝试循环,门放在循环前,一次定罪不重试)。
|
||||
# 非租户 Docker 任务不适用本门(见方法注释),不拦。
|
||||
_missing = self._missing_required_tools(task)
|
||||
if _missing:
|
||||
_tool_error = (
|
||||
"agent 声明必需工具缺失:%s(租户 codex-home 里"
|
||||
"没有对应 [mcp_servers.<tool>] 段;请在 worker"
|
||||
" 开启对应 MCP 后新建任务)" % ", ".join(_missing))
|
||||
self._set_status(task_id, "failed")
|
||||
self._write_execution_result(
|
||||
task_id, error_text=_tool_error,
|
||||
actual_model=self.model,
|
||||
started_at=started_at, finished_at=time.time(),
|
||||
only_if_status="failed")
|
||||
self._audit(task["project_id"], "worker_tool_gate_rejected",
|
||||
"%s: %s" % (task_id, _tool_error))
|
||||
log_event(self.logger, "worker_tool_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": _tool_error, "events": [],
|
||||
"cost_record_id": None}
|
||||
actual_model = task["model"]
|
||||
# 有限重试与真实 fallback 的优先级:取消不重试;一次尝试失败
|
||||
# 且可 fallback、且显式配了 fallback_models 时,优先真换模型
|
||||
|
||||
Reference in new issue
Block a user