feat: agent version pinning (v4)

resolve_registry_agent in cloud/agent_def.py: agent_name may be
<name> or <name>@<version>, agent_version may also be given
separately; both given must agree or 400. Unpinned loads
<name>.json (default); pinned loads <name>.<version>.json and
requires the file's own version field to match, with no silent
fallback to the default file. Version strings are restricted
to alphanumerics/._- (no path separators, no ..); the
agents-dir traversal guard is a second line of defence.
Server: agent_version is only for agent_name (inline agent
with agent_version is 400); stored agent_name is the base
name, not the @ form. Snapshot fixture:
agents/research-brief.v1.json (same content as the default
v1 file). Tests: test_agent_version.py (7 tests).
This commit is contained in:
host committed 2026-11-04 18:01:44 +00:00
1 parent 8fe4ad2f38
commit 7f60f12639
4 files changed
+277 -31

No files matched your search

+22 -31
View File
@@ -59,8 +59,8 @@ from urllib.parse import parse_qs, quote, urlparse
from cloud.agent_def import default_agents_dir as _agent_default_dir
from cloud.agent_def import list_known_skills as _agent_known_skills
from cloud.agent_def import load_agent_definition as _load_agent_definition
from cloud.agent_def import parse_agent_definition as _parse_agent_definition
from cloud.agent_def import resolve_registry_agent as _resolve_agent
from cloud.auth import ROLES as _MEMBER_ROLES
from cloud.auth import AuthStore
from cloud.coordinator import parse_parts as _coord_parse_parts
@@ -4409,18 +4409,30 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
return
if prompt_goal_target:
prompt = prompt_goal_target
# 声明式 agent 定义 v1/v2(见 cloud/agent_def.py):
# 声明式 agent 定义 v1/v2/v3(见 cloud/agent_def.py):
# inline agent 对象,或 agent_name(注册表 agents/ 下
# 的 <name>.json);两者不能同时给,歧义不猜。显式
# body 字段逐项优先于定义;解析出的字段落进已有列,
# 定义身份与指令落进 agent_name/agent_version/
# agent_instructions 列(v2),prompt 不再带头。
# 的 <name>.json,未钉选时即默认文件);两者不能同时
# 给,歧义不猜。版本钉选(v4):agent_version 字段,
# 或 agent_name 里的 <name>@<version> 写法,两者同给
# 时必须一致;钉选载 <name>.<version>.json 且文件内
# version 必须与请求一致,缺文件绝不回落默认文件。
# inline agent 自带 version,agent_version 只配
# agent_name用,两者同给即 400。显式 body 字段逐项
# 优先于定义;解析出的字段落进已有列,定义身份与
# 指令落进 agent_name/agent_version/agent_instructions
# 列(v2),prompt 不再带头。
definition = None
if body.get("agent") is not None \
and body.get("agent_name") is not None:
self._send_json(400, {
"error": "agent and agent_name cannot both be used"})
return
if body.get("agent") is not None \
and body.get("agent_version") is not None:
self._send_json(400, {
"error": "agent_version is only for agent_name;"
" an inline agent carries its own version"})
return
if body.get("agent") is not None:
if not isinstance(body.get("agent"), dict):
self._send_json(400, {"error": "agent must be an object"})
@@ -4433,32 +4445,11 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self._send_json(400, {"error": str(exc)})
return
if body.get("agent_name") is not None:
agent_name = body.get("agent_name")
if not isinstance(agent_name, str) or not agent_name.strip():
self._send_json(400, {
"error": "agent_name must be a non-empty string"})
return
try:
definition = _load_agent_definition(
agent_name.strip() + ".json",
agents_dir=_agent_default_dir())
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
return
# 注册表定义同样须过技能包校验:load 只校验了
# 格式(known_skills=None 时不查存在性),这里
# 用同一解析器补存在性校验,校验不过即 400。
try:
definition = _parse_agent_definition(
{
"name": definition.name,
"version": definition.version,
"description": definition.description,
"instructions": definition.instructions,
"skills": definition.skills,
"tools": definition.tools,
"defaults": definition.defaults,
},
definition = _resolve_agent(
body.get("agent_name"),
version=body.get("agent_version"),
agents_dir=_agent_default_dir(),
known_skills=_agent_known_skills())
except ValueError as exc:
self._send_json(400, {"error": str(exc)})