Converge manager out of product roles and drop dead inherit prefs.

Specialists-only binding/prewarm/MCP allowlists fold legacy manager aliases into generalist; stop persisting confirm/plan prefs and strip inherit from prompt signatures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-11 03:07:38 +08:00
parent bc20b41ee2
commit 454c841af7
20 changed files with 131 additions and 290 deletions

View file

@ -758,20 +758,13 @@ def _resolve_user_menu_chat_settings(
"""User-wide settings (⋯ menu): mode + specialist — all sessions share these keys.""" """User-wide settings (⋯ menu): mode + specialist — all sessions share these keys."""
user_mode_key = _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="interaction_mode") user_mode_key = _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="interaction_mode")
user_specialist_key = _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="specialist") user_specialist_key = _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="specialist")
user_confirm_strategy_key = _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="confirm_strategy")
user_plan_agent_version_key = _chat_user_mode_setting_key(
tenant_id=tenant_id, user_id=user_id, field="plan_agent_version"
)
mode_raw = str(store.get_setting(user_mode_key) or "").strip() mode_raw = str(store.get_setting(user_mode_key) or "").strip()
specialist_raw = str(store.get_setting(user_specialist_key) or "").strip() specialist_raw = str(store.get_setting(user_specialist_key) or "").strip()
confirm_raw = str(store.get_setting(user_confirm_strategy_key) or "").strip()
plan_agent_raw = str(store.get_setting(user_plan_agent_version_key) or "").strip()
interaction_mode = normalize_interaction_mode(mode_raw or "expert") interaction_mode = normalize_interaction_mode(mode_raw or "expert")
specialist = normalize_requested_specialist(specialist_raw or "generalist") specialist = normalize_requested_specialist(specialist_raw or "generalist")
specialist = _apply_specialist_flags(store, specialist) specialist = _apply_specialist_flags(store, specialist)
confirm_strategy = _normalize_confirm_strategy({"confirm_strategy": (confirm_raw or "strict")}) # Legacy plan-mode fields kept as wire constants for older clients.
plan_agent_version = _normalize_plan_agent_version({"plan_agent_version": (plan_agent_raw or "v1")}) return interaction_mode, specialist, "strict", "v1"
return interaction_mode, specialist, confirm_strategy, plan_agent_version
def _persist_user_menu_chat_settings( def _persist_user_menu_chat_settings(
@ -784,6 +777,7 @@ def _persist_user_menu_chat_settings(
confirm_strategy: str, confirm_strategy: str,
plan_agent_version: str, plan_agent_version: str,
) -> None: ) -> None:
del confirm_strategy, plan_agent_version
store.set_setting( store.set_setting(
_chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="interaction_mode"), _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="interaction_mode"),
interaction_mode, interaction_mode,
@ -792,15 +786,6 @@ def _persist_user_menu_chat_settings(
_chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="specialist"), _chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="specialist"),
specialist, specialist,
) )
store.set_setting(
_chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="confirm_strategy"),
confirm_strategy,
)
store.set_setting(
_chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="plan_agent_version"),
plan_agent_version,
)
store.set_setting("AIA_EXPERT_PLAN_CONFIRM_STRATEGY", confirm_strategy)
def _resolve_session_dialog_chat_settings( def _resolve_session_dialog_chat_settings(

View file

@ -440,17 +440,15 @@ def build_admin_router() -> APIRouter:
def _ordered_specialists() -> list[str]: def _ordered_specialists() -> list[str]:
base = [str(k).strip().lower() for k in discover_specialist_ids() if str(k).strip()] base = [str(k).strip().lower() for k in discover_specialist_ids() if str(k).strip()]
preferred = [x for x in ("generalist", "ops", "image") if x in set(base)] preferred = [x for x in ("generalist", "ops", "memory") if x in set(base)]
return preferred + [x for x in base if x not in set(preferred)] return preferred + [x for x in base if x not in set(preferred)]
def _ordered_mcp_roles() -> list[str]: def _ordered_mcp_roles() -> list[str]:
specs = _ordered_specialists() return _ordered_specialists()
return ["manager", *[x for x in specs if x != "manager"]]
def _ordered_roles() -> list[str]: def _ordered_roles() -> list[str]:
"""Canonical role list used by Admin preview APIs.""" """Canonical role list used by Admin preview APIs."""
specs = _ordered_specialists() return _ordered_specialists()
return ["manager", *[x for x in specs if x != "manager"]]
_EXPERT_ALLOWED_FILES = {"SOUL.md", "ROLE_SYSTEM.md"} _EXPERT_ALLOWED_FILES = {"SOUL.md", "ROLE_SYSTEM.md"}
@ -3197,7 +3195,7 @@ def build_admin_router() -> APIRouter:
ctx = _resolve_auth(store, authorization) ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write") _require_permission(ctx, "admin:tenant:write")
available = _ordered_mcp_roles() available = _ordered_mcp_roles()
raw = str(store.get_setting("mcp_allowed_specialists") or "").strip() or "generalist,manager,ops" raw = str(store.get_setting("mcp_allowed_specialists") or "").strip() or "generalist,ops"
allowed = [x.strip().lower() for x in raw.split(",") if x.strip()] allowed = [x.strip().lower() for x in raw.split(",") if x.strip()]
allowed_set = set(allowed) allowed_set = set(allowed)
ordered = [x for x in available if x in allowed_set] ordered = [x for x in available if x in allowed_set]
@ -3220,7 +3218,7 @@ def build_admin_router() -> APIRouter:
items = raw if isinstance(raw, list) else [] items = raw if isinstance(raw, list) else []
allowset = {str(x).strip().lower() for x in items if str(x).strip().lower() in available_set} allowset = {str(x).strip().lower() for x in items if str(x).strip().lower() in available_set}
if not allowset: if not allowset:
defaults = {x for x in ("generalist", "manager") if x in available_set} defaults = {x for x in ("generalist", "ops") if x in available_set}
allowset = defaults if defaults else (set(available[:1]) if available else set()) allowset = defaults if defaults else (set(available[:1]) if available else set())
ordered = [x for x in available if x in allowset] ordered = [x for x in available if x in allowset]
store.set_setting("mcp_allowed_specialists", ",".join(ordered)) store.set_setting("mcp_allowed_specialists", ",".join(ordered))

View file

@ -22,7 +22,6 @@ from runtime.skill_installer import (
from runtime.skill_role_binding import ( from runtime.skill_role_binding import (
SKILL_ROLE_BINDING_ENABLED_SETTING, SKILL_ROLE_BINDING_ENABLED_SETTING,
SKILL_ROLE_BINDING_KEY, SKILL_ROLE_BINDING_KEY,
SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING,
load_skill_role_binding_dict, load_skill_role_binding_dict,
normalize_skill_role_binding, normalize_skill_role_binding,
ordered_binding_roles, ordered_binding_roles,
@ -348,7 +347,7 @@ def include_skill_routes(
"enabled": bool(skill_role_binding_enabled(store=store)), "enabled": bool(skill_role_binding_enabled(store=store)),
"enabled_stored": bool(skill_role_binding_enabled_stored(store=store)), "enabled_stored": bool(skill_role_binding_enabled_stored(store=store)),
"enabled_env_present": bool(skill_role_binding_enabled_env_present()), "enabled_env_present": bool(skill_role_binding_enabled_env_present()),
"manager_inherit": str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip() not in {"0", "false", "False"}, "manager_inherit": False,
"available_roles": roles, "available_roles": roles,
"installed_skills": items, "installed_skills": items,
"mapping": mapping, "mapping": mapping,
@ -365,8 +364,6 @@ def include_skill_routes(
_require_tenant_write(ctx) _require_tenant_write(ctx)
if "enabled" in payload: if "enabled" in payload:
store.set_setting(SKILL_ROLE_BINDING_ENABLED_SETTING, "1" if bool(payload.get("enabled")) else "0") store.set_setting(SKILL_ROLE_BINDING_ENABLED_SETTING, "1" if bool(payload.get("enabled")) else "0")
if "manager_inherit" in payload:
store.set_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING, "1" if bool(payload.get("manager_inherit")) else "0")
roles, _prev_mapping, valid = _normalized_skill_binding(store) roles, _prev_mapping, valid = _normalized_skill_binding(store)
mapping_raw = payload.get("mapping") if isinstance(payload.get("mapping"), dict) else {} mapping_raw = payload.get("mapping") if isinstance(payload.get("mapping"), dict) else {}
mapping = normalize_skill_role_binding( mapping = normalize_skill_role_binding(
@ -388,7 +385,7 @@ def include_skill_routes(
"enabled": bool(skill_role_binding_enabled(store=store)), "enabled": bool(skill_role_binding_enabled(store=store)),
"enabled_stored": bool(skill_role_binding_enabled_stored(store=store)), "enabled_stored": bool(skill_role_binding_enabled_stored(store=store)),
"enabled_env_present": bool(skill_role_binding_enabled_env_present()), "enabled_env_present": bool(skill_role_binding_enabled_env_present()),
"manager_inherit": str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip() not in {"0", "false", "False"}, "manager_inherit": False,
"available_roles": roles, "available_roles": roles,
"mapping": mapping, "mapping": mapping,
} }
@ -401,7 +398,7 @@ def include_skill_routes(
roles, mapping, _valid = _normalized_skill_binding(store) roles, mapping, _valid = _normalized_skill_binding(store)
role_rows: list[dict[str, Any]] = [] role_rows: list[dict[str, Any]] = []
for role in roles: for role in roles:
specialist = "generalist" if role == "manager" else role specialist = role
ex = build_gateway_executor(store=store, specialist=specialist) ex = build_gateway_executor(store=store, specialist=specialist)
tools = getattr(ex, "tools", None) tools = getattr(ex, "tools", None)
model = getattr(ex, "model", None) model = getattr(ex, "model", None)
@ -432,10 +429,8 @@ def include_skill_routes(
base_url=base_url, base_url=base_url,
) )
direct_set = set(mapping.get(role) or []) direct_set = set(mapping.get(role) or [])
manager_set = set(mapping.get("manager") or [])
workspace_total = 0 workspace_total = 0
workspace_direct = 0 workspace_direct = 0
workspace_inherited = 0
workspace_resolved_tool_match = 0 workspace_resolved_tool_match = 0
workspace_docs_only = 0 workspace_docs_only = 0
mcp_total = 0 mcp_total = 0
@ -456,8 +451,6 @@ def include_skill_routes(
docs_only_names.append(str(nm)) docs_only_names.append(str(nm))
if nm in direct_set: if nm in direct_set:
workspace_direct += 1 workspace_direct += 1
elif role != "manager" and nm in manager_set:
workspace_inherited += 1
elif str(nm).startswith("mcp__"): elif str(nm).startswith("mcp__"):
mcp_total += 1 mcp_total += 1
else: else:
@ -468,7 +461,7 @@ def include_skill_routes(
"total": len(entries), "total": len(entries),
"workspace_total": workspace_total, "workspace_total": workspace_total,
"workspace_direct": workspace_direct, "workspace_direct": workspace_direct,
"workspace_inherited_manager": workspace_inherited, "workspace_inherited_manager": 0,
"workspace_resolved_tool_match": workspace_resolved_tool_match, "workspace_resolved_tool_match": workspace_resolved_tool_match,
"workspace_docs_only": workspace_docs_only, "workspace_docs_only": workspace_docs_only,
"mcp_total": mcp_total, "mcp_total": mcp_total,

View file

@ -1583,7 +1583,7 @@ async function renderStack() {
apiGetNoHang("/admin/api/runtime/anomalies"), apiGetNoHang("/admin/api/runtime/anomalies"),
apiGetNoHang("/admin/api/runtime/scan-artifacts"), apiGetNoHang("/admin/api/runtime/scan-artifacts"),
apiGetNoHang("/admin/api/runtime/prewarm/status"), apiGetNoHang("/admin/api/runtime/prewarm/status"),
apiGetNoHang("/admin/api/runtime/prewarm/prompts?role=manager"), apiGetNoHang("/admin/api/runtime/prewarm/prompts?role=generalist"),
apiGetNoHang("/admin/api/chat/settings/specialist-flags"), apiGetNoHang("/admin/api/chat/settings/specialist-flags"),
apiGetNoHang("/admin/api/chat/settings/channel-dispatch/weixin"), apiGetNoHang("/admin/api/chat/settings/channel-dispatch/weixin"),
apiGetNoHang("/admin/api/chat/settings/channel-dispatch/whatsapp"), apiGetNoHang("/admin/api/chat/settings/channel-dispatch/whatsapp"),
@ -2220,7 +2220,7 @@ async function renderStack() {
: [el("tr", {}, [el("td", { class: "muted", text: "—", colspan: "5" })])]; : [el("tr", {}, [el("td", { class: "muted", text: "—", colspan: "5" })])];
const promptsSectionBody = el("div"); const promptsSectionBody = el("div");
const promptsRoleSelect = el("select", { class: "input" }, [ const promptsRoleSelect = el("select", { class: "input" }, [
el("option", { value: "manager", text: "manager (default)" }), el("option", { value: "generalist", text: "generalist (default)" }),
el("option", { value: "", text: currentLang === "zh" ? "全部角色" : "all roles" }), el("option", { value: "", text: currentLang === "zh" ? "全部角色" : "all roles" }),
]); ]);
const renderPromptCards = (resp) => { const renderPromptCards = (resp) => {
@ -2327,7 +2327,7 @@ async function renderStack() {
]), ]),
el("div", { class: "card" }, [ el("div", { class: "card" }, [
el("div", { class: "card__title", text: currentLang === "zh" ? "预热后提示词(按专家)" : "Prewarmed Prompts by Role" }), el("div", { class: "card__title", text: currentLang === "zh" ? "预热后提示词(按专家)" : "Prewarmed Prompts by Role" }),
el("div", { class: "muted", text: currentLang === "zh" ? "展示 manager + 各专家预热后的提示词内容。" : "Shows prewarmed prompt content for manager and specialists." }), el("div", { class: "muted", text: currentLang === "zh" ? "展示各专家预热后的提示词内容。" : "Shows prewarmed prompt content for specialists." }),
el("div", { class: "row" }, [ el("div", { class: "row" }, [
el("label", { text: currentLang === "zh" ? "角色筛选" : "Role filter" }), el("label", { text: currentLang === "zh" ? "角色筛选" : "Role filter" }),
promptsRoleSelect, promptsRoleSelect,
@ -7794,7 +7794,7 @@ async function renderSkills() {
marketDetailPre, marketDetailPre,
]); ]);
const skillBindingState = { roles: [], names: [], mapping: {}, enabled: false, managerInherit: true }; const skillBindingState = { roles: [], names: [], mapping: {}, enabled: false };
const skillBindingStatus = el("div", { class: "muted", text: "" }); const skillBindingStatus = el("div", { class: "muted", text: "" });
const skillBindingPersistHint = el("div", { const skillBindingPersistHint = el("div", {
class: "muted", class: "muted",
@ -7807,7 +7807,6 @@ async function renderSkills() {
}); });
const skillEffectiveState = { items: [] }; const skillEffectiveState = { items: [] };
const skillBindingEnabledCb = el("input", { type: "checkbox" }); const skillBindingEnabledCb = el("input", { type: "checkbox" });
const skillBindingManagerInheritCb = el("input", { type: "checkbox" });
const skillRoleSelect = el("select", { class: "input" }, []); const skillRoleSelect = el("select", { class: "input" }, []);
const skillBindingListWrap = el("div"); const skillBindingListWrap = el("div");
const skillBindingDashTbody = el("tbody"); const skillBindingDashTbody = el("tbody");
@ -7816,11 +7815,9 @@ async function renderSkills() {
skillBindingDashTbody.innerHTML = ""; skillBindingDashTbody.innerHTML = "";
const roles = Array.isArray(skillBindingState.roles) ? skillBindingState.roles : []; const roles = Array.isArray(skillBindingState.roles) ? skillBindingState.roles : [];
const mapping = skillBindingState.mapping && typeof skillBindingState.mapping === "object" ? skillBindingState.mapping : {}; const mapping = skillBindingState.mapping && typeof skillBindingState.mapping === "object" ? skillBindingState.mapping : {};
const managerBound = new Set(Array.isArray(mapping.manager) ? mapping.manager.map((x) => String(x)) : []);
const inheritManager = !!skillBindingState.managerInherit;
roles.forEach((role) => { roles.forEach((role) => {
const direct = new Set(Array.isArray(mapping[role]) ? mapping[role].map((x) => String(x)) : []); const direct = new Set(Array.isArray(mapping[role]) ? mapping[role].map((x) => String(x)) : []);
const effective = new Set([...(role === "manager" || !inheritManager ? [] : Array.from(managerBound)), ...Array.from(direct)]); const effective = new Set(Array.from(direct));
skillBindingDashTbody.appendChild( skillBindingDashTbody.appendChild(
el("tr", {}, [ el("tr", {}, [
el("td", { text: role }), el("td", { text: role }),
@ -7860,7 +7857,6 @@ async function renderSkills() {
el("td", { text: String(x.total || 0) }), el("td", { text: String(x.total || 0) }),
el("td", { text: String(x.workspace_total || 0) }), el("td", { text: String(x.workspace_total || 0) }),
el("td", { text: String(x.workspace_direct || 0) }), el("td", { text: String(x.workspace_direct || 0) }),
el("td", { text: String(x.workspace_inherited_manager || 0) }),
el("td", { text: String(x.workspace_resolved_tool_match || 0) }), el("td", { text: String(x.workspace_resolved_tool_match || 0) }),
el("td", { text: String(x.workspace_docs_only || 0) }), el("td", { text: String(x.workspace_docs_only || 0) }),
el("td", { text: String(x.mcp_total || 0) }), el("td", { text: String(x.mcp_total || 0) }),
@ -7871,7 +7867,7 @@ async function renderSkills() {
); );
}); });
if (!rows.length) { if (!rows.length) {
skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "11" })])); skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "10" })]));
} }
}; };
const loadSkillEffective = async () => { const loadSkillEffective = async () => {
@ -7920,9 +7916,7 @@ async function renderSkills() {
.filter(Boolean); .filter(Boolean);
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {}; skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled; skillBindingState.enabled = !!r.enabled;
skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true;
skillBindingEnabledCb.checked = skillBindingState.enabled; skillBindingEnabledCb.checked = skillBindingState.enabled;
skillBindingManagerInheritCb.checked = skillBindingState.managerInherit;
if (r.enabled_env_present) { if (r.enabled_env_present) {
skillBindingEnvHint.style.display = "block"; skillBindingEnvHint.style.display = "block";
const stored = !!r.enabled_stored; const stored = !!r.enabled_stored;
@ -7959,14 +7953,11 @@ async function renderSkills() {
try { try {
const r = await apiPost("/admin/api/skills/binding", { const r = await apiPost("/admin/api/skills/binding", {
enabled: !!skillBindingEnabledCb.checked, enabled: !!skillBindingEnabledCb.checked,
manager_inherit: !!skillBindingManagerInheritCb.checked,
mapping: skillBindingState.mapping, mapping: skillBindingState.mapping,
}); });
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {}; skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled; skillBindingState.enabled = !!r.enabled;
skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true;
skillBindingEnabledCb.checked = skillBindingState.enabled; skillBindingEnabledCb.checked = skillBindingState.enabled;
skillBindingManagerInheritCb.checked = skillBindingState.managerInherit;
if (r.enabled_env_present) { if (r.enabled_env_present) {
skillBindingEnvHint.style.display = "block"; skillBindingEnvHint.style.display = "block";
const stored = !!r.enabled_stored; const stored = !!r.enabled_stored;
@ -7979,7 +7970,7 @@ async function renderSkills() {
skillBindingEnvHint.style.display = "none"; skillBindingEnvHint.style.display = "none";
skillBindingEnvHint.textContent = ""; skillBindingEnvHint.textContent = "";
} }
skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)} manager_inherit=${String(skillBindingState.managerInherit)}`; skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)}`;
renderSkillBindingList(); renderSkillBindingList();
renderSkillBindingDashboard(); renderSkillBindingDashboard();
await loadSkillEffective(); await loadSkillEffective();
@ -7989,12 +7980,12 @@ async function renderSkills() {
}, },
}); });
const skillBindingBox = el("details", { style: "margin:10px 0 14px 0;" }, [ const skillBindingBox = el("details", { style: "margin:10px 0 14px 0;" }, [
el("summary", { text: "Skill role binding (manager + specialists)", style: "cursor:pointer;user-select:none;" }), el("summary", { text: "Skill role binding (specialists)", style: "cursor:pointer;user-select:none;" }),
el("div", { el("div", {
class: "muted", class: "muted",
style: "margin:8px 0;line-height:1.5;", style: "margin:8px 0;line-height:1.5;",
text: text:
"When enabled, the model skills catalog is filtered per role: each role sees only skills bound to that role (and optionally manager-bound skills), plus skills under skills/_workspace/public/. If nothing is bound yet, non-public skills are hidden until you assign them.", "When enabled, the model skills catalog is filtered per role: each role sees only skills bound to that role, plus skills under skills/_workspace/public/. If nothing is bound yet, non-public skills are hidden until you assign them.",
}), }),
skillBindingStatus, skillBindingStatus,
skillBindingPersistHint, skillBindingPersistHint,
@ -8003,10 +7994,6 @@ async function renderSkills() {
skillBindingEnabledCb, skillBindingEnabledCb,
el("span", { text: "Enable role binding (AIA_SKILL_ROLE_BINDING_ENABLED)" }), el("span", { text: "Enable role binding (AIA_SKILL_ROLE_BINDING_ENABLED)" }),
]), ]),
el("label", { class: "row", style: "gap:8px;align-items:center;margin-top:6px;" }, [
skillBindingManagerInheritCb,
el("span", { text: "Inherit manager skills to other roles (AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT)" }),
]),
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center;" }, [ el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center;" }, [
el("label", { text: "Role" }), el("label", { text: "Role" }),
skillRoleSelect, skillRoleSelect,
@ -8019,7 +8006,7 @@ async function renderSkills() {
skillBindingDashTbody, skillBindingDashTbody,
]), ]),
]), ]),
el("div", { class: "muted", style: "margin:8px 0 4px 0;", text: "Effective skills dashboard (binding + inherited + MCP + tools)" }), el("div", { class: "muted", style: "margin:8px 0 4px 0;", text: "Effective skills dashboard (binding + MCP + tools)" }),
el("div", { class: "table-wrap" }, [ el("div", { class: "table-wrap" }, [
el("table", { class: "table table--compact" }, [ el("table", { class: "table table--compact" }, [
el("thead", {}, [el("tr", {}, [ el("thead", {}, [el("tr", {}, [
@ -8027,7 +8014,6 @@ async function renderSkills() {
el("th", { text: "total effective" }), el("th", { text: "total effective" }),
el("th", { text: "workspace" }), el("th", { text: "workspace" }),
el("th", { text: "direct bind" }), el("th", { text: "direct bind" }),
el("th", { text: "inherited manager" }),
el("th", { text: "workspace resolved" }), el("th", { text: "workspace resolved" }),
el("th", { text: "workspace docs-only" }), el("th", { text: "workspace docs-only" }),
el("th", { text: "mcp converted" }), el("th", { text: "mcp converted" }),
@ -8165,9 +8151,8 @@ async function renderSkills() {
const rolesTotal = Number(body.roles_total || 0); const rolesTotal = Number(body.roles_total || 0);
const totalInternal = Number(summary.total_internal_tools || 0); const totalInternal = Number(summary.total_internal_tools || 0);
const totalWired = Number(summary.total_wired_tools || 0); const totalWired = Number(summary.total_wired_tools || 0);
const totalPermBan = Number(summary.total_perm_ban_9999 || 0);
const items = Array.isArray(body.items) ? body.items : []; const items = Array.isArray(body.items) ? body.items : [];
selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired} permBan9999=${totalPermBan}`; selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired}`;
selfCheckTbody.innerHTML = ""; selfCheckTbody.innerHTML = "";
items.forEach((x) => { items.forEach((x) => {
const removedMcp = Number(x.removed_mcp_total || 0); const removedMcp = Number(x.removed_mcp_total || 0);
@ -8194,7 +8179,6 @@ async function renderSkills() {
el("td", { text: String(x.raw_count || 0) }), el("td", { text: String(x.raw_count || 0) }),
el("td", { text: String(x.wired_count || 0) }), el("td", { text: String(x.wired_count || 0) }),
el("td", { text: String(x.removed_mcp_total || 0) }), el("td", { text: String(x.removed_mcp_total || 0) }),
el("td", { text: String(x.policy_perm_ban_9999 || 0) }),
]), ]),
); );
}); });
@ -8252,7 +8236,6 @@ async function renderSkills() {
el("th", { text: "raw" }), el("th", { text: "raw" }),
el("th", { text: "wired" }), el("th", { text: "wired" }),
el("th", { text: "removed mcp" }), el("th", { text: "removed mcp" }),
el("th", { text: "perm ban(9999)" }),
])]), ])]),
selfCheckTbody, selfCheckTbody,
]), ]),
@ -8888,7 +8871,7 @@ async function renderSkills() {
await loadAudits(); await loadAudits();
await loadSkillBinding(); await loadSkillBinding();
await loadSkillMode(); await loadSkillMode();
const rolesForPreview = (Array.isArray(skillBindingState.roles) && skillBindingState.roles.length ? skillBindingState.roles : ["generalist", "ops", "image", "manager"]).map((x) => String(x)); const rolesForPreview = (Array.isArray(skillBindingState.roles) && skillBindingState.roles.length ? skillBindingState.roles : ["generalist", "ops", "memory"]).map((x) => String(x));
internalRoleSelect.innerHTML = ""; internalRoleSelect.innerHTML = "";
rolesForPreview.forEach((role) => internalRoleSelect.appendChild(el("option", { value: role, text: role }))); rolesForPreview.forEach((role) => internalRoleSelect.appendChild(el("option", { value: role, text: role })));
internalRoleSelect.value = rolesForPreview.includes("generalist") ? "generalist" : rolesForPreview[0]; internalRoleSelect.value = rolesForPreview.includes("generalist") ? "generalist" : rolesForPreview[0];

View file

@ -24,8 +24,8 @@ def _read_text(path: Path) -> str:
def _workspace_for_role(role: str) -> str: def _workspace_for_role(role: str) -> str:
r = str(role or "").strip().lower() r = str(role or "").strip().lower()
if r == "manager": if r in {"manager", "manager_self", "main"}:
return "main" return "generalist"
return r or "generalist" return r or "generalist"

View file

@ -11,7 +11,6 @@ from runtime.agents.specialists import (
SpecialistProfile, SpecialistProfile,
normalize_specialist_id, normalize_specialist_id,
agent_role_ids, agent_role_ids,
MANAGER_AGENT_ID,
specialist_ids, specialist_ids,
default_system_prefix_for_specialist, default_system_prefix_for_specialist,
default_tool_tags_for_specialist, default_tool_tags_for_specialist,
@ -196,7 +195,7 @@ def _build_executor_components(
return active_model, active_mode return active_model, active_mode
return _build_chat_model_for_profile(bound_pid, allow_runtime_overrides=False) return _build_chat_model_for_profile(bound_pid, allow_runtime_overrides=False)
manager_model, manager_mode = _pick_model_for_role(MANAGER_AGENT_ID) manager_model, manager_mode = _pick_model_for_role("generalist")
specialist_models: dict[str, object] = {} specialist_models: dict[str, object] = {}
specialist_modes: dict[str, str] = {} specialist_modes: dict[str, str] = {}
for sid in specialist_ids(): for sid in specialist_ids():

View file

@ -69,7 +69,7 @@ def specialist_ids() -> tuple[SpecialistId, ...]:
def agent_role_ids() -> tuple[AgentRoleId, ...]: def agent_role_ids() -> tuple[AgentRoleId, ...]:
return (MANAGER_AGENT_ID, *specialist_ids()) return specialist_ids()
def expert_name_for_specialist(specialist_id: SpecialistId) -> str: def expert_name_for_specialist(specialist_id: SpecialistId) -> str:
@ -101,6 +101,8 @@ def model_role_for_specialist(specialist_id: SpecialistId) -> AgentRoleId:
def normalize_specialist_id(specialist_id: SpecialistId | None) -> SpecialistId: def normalize_specialist_id(specialist_id: SpecialistId | None) -> SpecialistId:
sid = (specialist_id or "").strip().lower() sid = (specialist_id or "").strip().lower()
if sid in {"manager", "manager_self", "main", "comprehensive"}:
return "generalist"
if sid in _REMOVED_SPECIALIST_IDS: if sid in _REMOVED_SPECIALIST_IDS:
return "generalist" return "generalist"
if sid in SPECIALISTS: if sid in SPECIALISTS:
@ -125,6 +127,11 @@ def parse_agent_profile_bindings(raw: str | None) -> dict[AgentRoleId, str]:
return out return out
if not isinstance(obj, dict): if not isinstance(obj, dict):
return out return out
# Fold legacy manager profile into generalist when generalist empty.
mgr = str(obj.get("manager") or "").strip()
if mgr and not str(obj.get("generalist") or "").strip():
obj = dict(obj)
obj["generalist"] = mgr
for rid in agent_role_ids(): for rid in agent_role_ids():
v = obj.get(rid) v = obj.get(rid)
if v is None: if v is None:

View file

@ -171,7 +171,6 @@ def _tool_wire_settings_signature(store: Any) -> tuple[bool, str]:
f"skill_rt={str(store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}", f"skill_rt={str(store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}",
f"skill_disabled={str(store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}", f"skill_disabled={str(store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}",
f"bind_en={str(store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}", f"bind_en={str(store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}",
f"bind_inherit={str(store.get_setting('AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT') or '')}",
] ]
) )
return runtime_enabled, sig return runtime_enabled, sig

View file

@ -519,8 +519,7 @@ class OclawGateway:
cache_key = ( cache_key = (
f"base={base_url}|skill_rt={str(self.store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}|" f"base={base_url}|skill_rt={str(self.store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}|"
f"skill_disabled={str(self.store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}|" f"skill_disabled={str(self.store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}|"
f"bind_en={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}|" f"bind_en={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}"
f"bind_inherit={str(self.store.get_setting('AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT') or '')}"
) )
now = time.time() now = time.time()
with _SKILL_MANIFEST_CACHE_LOCK: with _SKILL_MANIFEST_CACHE_LOCK:

View file

@ -8,14 +8,9 @@ from svc.persistence.assistant_store import get_assistant_store
from runtime.agent_context import build_role_system_context from runtime.agent_context import build_role_system_context
from runtime.agents.specialists import discover_specialist_ids from runtime.agents.specialists import discover_specialist_ids
from runtime.direct_loop import tool_wire_freeze_status, warm_tool_wire_cache from runtime.direct_loop import tool_wire_freeze_status, warm_tool_wire_cache
from runtime.skill_role_binding import SKILL_ROLE_BINDING_KEY
from runtime.skills import workspace_skills_layout_signature
from runtime.system_prompt import get_executor_prompt_static, warm_executor_prompt_cache from runtime.system_prompt import get_executor_prompt_static, warm_executor_prompt_cache
from runtime.tools.catalog import default_registry from runtime.tools.catalog import default_registry
from runtime.workspaces.experts import expert_workspace_signature_token, list_experts
_MANAGER_PREBUILD_CACHE_LOCK = threading.Lock()
_MANAGER_PREBUILD_CACHE: dict[tuple[Any, ...], dict[str, Any]] = {}
_RUNTIME_PREWARM_LOCK = threading.Lock() _RUNTIME_PREWARM_LOCK = threading.Lock()
_RUNTIME_PREWARM_RUNNING = False _RUNTIME_PREWARM_RUNNING = False
_RUNTIME_PREWARM_LAST: dict[str, Any] = { _RUNTIME_PREWARM_LAST: dict[str, Any] = {
@ -31,47 +26,6 @@ _RUNTIME_PREWARM_HISTORY: list[dict[str, Any]] = []
_RUNTIME_PREWARM_HISTORY_LIMIT = 40 _RUNTIME_PREWARM_HISTORY_LIMIT = 40
def _manager_settings_signature(store: Any) -> tuple[str, ...]:
keys = (
"AIA_SKILL_RUNTIME_ENABLED",
"AIA_SKILL_DISABLED_NAMES",
"AIA_SKILL_ROLE_BINDING_ENABLED",
"AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT",
SKILL_ROLE_BINDING_KEY,
"AIA_CHAT_SPECIALIST_FLAGS_JSON",
)
parts: list[str] = []
for key in keys:
try:
val = str(store.get_setting(key) or "")
except Exception:
val = ""
parts.append(f"{key}={val}")
return tuple(parts)
def _compact_line(text: str, *, limit: int = 80) -> str:
s = " ".join(str(text or "").strip().split())
if len(s) <= limit:
return s
return s[: max(0, limit - 1)] + "…"
def _build_expert_desc_map() -> dict[str, str]:
out: dict[str, str] = {}
for row in list_experts():
eid = str(row.get("id") or "").strip().lower()
if not eid:
continue
files = row.get("files") if isinstance(row, dict) else {}
if not isinstance(files, dict):
continue
desc = _compact_line(str(files.get("ROLE_SYSTEM.md") or ""), limit=80)
if desc:
out[eid] = desc
return out
def get_manager_prompt_prebuild( def get_manager_prompt_prebuild(
*, *,
store: Any, store: Any,
@ -79,18 +33,8 @@ def get_manager_prompt_prebuild(
base_url: str, base_url: str,
memory_enabled: bool, memory_enabled: bool,
) -> dict[str, Any]: ) -> dict[str, Any]:
cache_key = ( """Legacy stub: Manager prompt packing removed; return specialist catalog only."""
str(base_url or "").strip(), del store, registry, base_url
bool(memory_enabled),
expert_workspace_signature_token(),
_manager_settings_signature(store),
workspace_skills_layout_signature(),
)
with _MANAGER_PREBUILD_CACHE_LOCK:
cached = _MANAGER_PREBUILD_CACHE.get(cache_key)
if isinstance(cached, dict):
return dict(cached)
allowed_fixed = [str(x).strip().lower() for x in discover_specialist_ids() if str(x).strip()] allowed_fixed = [str(x).strip().lower() for x in discover_specialist_ids() if str(x).strip()]
if not allowed_fixed: if not allowed_fixed:
allowed_fixed = ["generalist"] allowed_fixed = ["generalist"]
@ -99,38 +43,17 @@ def get_manager_prompt_prebuild(
if "generalist" not in allowed_fixed: if "generalist" not in allowed_fixed:
allowed_fixed.insert(0, "generalist") allowed_fixed.insert(0, "generalist")
allowed_fixed_quoted = ", ".join([f'"{x}"' for x in allowed_fixed]) allowed_fixed_quoted = ", ".join([f'"{x}"' for x in allowed_fixed])
return {
desc_by_id = _build_expert_desc_map() "manager_context": "",
candidate_lines = [f"- {sid}: {desc_by_id.get(sid) or 'no description'}" for sid in allowed_fixed]
dynamic_hint = (
f"\n{chr(10).join(candidate_lines)}\n"
"不要假设固定专家集合。"
)
manager_context = build_role_system_context(
"manager",
template_vars={"MANAGER_DYNAMIC_EXPERTS_HINT": dynamic_hint},
)
out = {
"manager_context": manager_context,
"allowed_fixed": tuple(allowed_fixed), "allowed_fixed": tuple(allowed_fixed),
"allowed_fixed_quoted": allowed_fixed_quoted, "allowed_fixed_quoted": allowed_fixed_quoted,
} }
with _MANAGER_PREBUILD_CACHE_LOCK:
_MANAGER_PREBUILD_CACHE[cache_key] = dict(out)
if len(_MANAGER_PREBUILD_CACHE) > 64:
_MANAGER_PREBUILD_CACHE.clear()
return out
def warm_startup_prompt_prebuild(*, store: Any, registry: Any, base_url: str, memory_enabled: bool) -> dict[str, Any]: def warm_startup_prompt_prebuild(*, store: Any, registry: Any, base_url: str, memory_enabled: bool) -> dict[str, Any]:
del memory_enabled
t0 = time.perf_counter() t0 = time.perf_counter()
manager_pack = get_manager_prompt_prebuild( role_systems: dict[str, str] = {}
store=store,
registry=registry,
base_url=base_url,
memory_enabled=memory_enabled,
)
role_systems: dict[str, str] = {"manager": str(manager_pack.get("manager_context") or "")}
for sid in discover_specialist_ids(): for sid in discover_specialist_ids():
role_systems[str(sid)] = build_role_system_context(str(sid)) role_systems[str(sid)] = build_role_system_context(str(sid))
role_warm = warm_executor_prompt_cache( role_warm = warm_executor_prompt_cache(
@ -144,7 +67,7 @@ def warm_startup_prompt_prebuild(*, store: Any, registry: Any, base_url: str, me
return { return {
"ok": True, "ok": True,
"elapsed_ms": elapsed_ms, "elapsed_ms": elapsed_ms,
"manager_candidates": int(len(manager_pack.get("allowed_fixed") or [])), "manager_candidates": 0,
"roles_warmed": int(role_warm.get("roles_warmed") or 0), "roles_warmed": int(role_warm.get("roles_warmed") or 0),
} }
@ -187,7 +110,7 @@ def run_runtime_prewarm(
base_url=base_url, base_url=base_url,
memory_enabled=bool(memory_enabled), memory_enabled=bool(memory_enabled),
) )
roles = ["manager", *list(discover_specialist_ids())] roles = list(discover_specialist_ids())
tool_stats = warm_tool_wire_cache( tool_stats = warm_tool_wire_cache(
store=own_store, store=own_store,
tools=registry, tools=registry,
@ -245,24 +168,16 @@ def runtime_prewarm_prompts_snapshot(
own_store = store if store is not None else get_assistant_store() own_store = store if store is not None else get_assistant_store()
registry = default_registry(store=own_store) registry = default_registry(store=own_store)
target = str(role or "").strip().lower() target = str(role or "").strip().lower()
allowed_roles = ["manager", *list(discover_specialist_ids())] if target in {"manager", "manager_self", "main"}:
target = "generalist"
allowed_roles = list(discover_specialist_ids())
if target and target not in allowed_roles: if target and target not in allowed_roles:
return {"ok": False, "error": "invalid_role", "allowed_roles": allowed_roles} return {"ok": False, "error": "invalid_role", "allowed_roles": allowed_roles}
selected_roles = [target] if target else allowed_roles selected_roles = [target] if target else allowed_roles
manager_pack = get_manager_prompt_prebuild(
store=own_store,
registry=registry,
base_url=base_url,
memory_enabled=memory_enabled,
)
prompts: dict[str, dict[str, Any]] = {} prompts: dict[str, dict[str, Any]] = {}
for rid in selected_roles: for rid in selected_roles:
base_system = ( base_system = build_role_system_context(str(rid))
str(manager_pack.get("manager_context") or "")
if rid == "manager"
else build_role_system_context(str(rid))
)
executor_system = get_executor_prompt_static( executor_system = get_executor_prompt_static(
store=own_store, store=own_store,
tools=registry, tools=registry,

View file

@ -8,6 +8,7 @@ from runtime.agents.specialists import discover_specialist_ids
SKILL_ROLE_BINDING_KEY = "skill_role_binding" SKILL_ROLE_BINDING_KEY = "skill_role_binding"
SKILL_ROLE_BINDING_ENABLED_SETTING = "AIA_SKILL_ROLE_BINDING_ENABLED" SKILL_ROLE_BINDING_ENABLED_SETTING = "AIA_SKILL_ROLE_BINDING_ENABLED"
# Deprecated: Manager inherit removed; kept so old settings keys do not crash readers.
SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING = "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT" SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING = "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT"
@ -22,7 +23,8 @@ def ordered_specialist_ids() -> list[str]:
def ordered_binding_roles() -> list[str]: def ordered_binding_roles() -> list[str]:
return ["manager", *ordered_specialist_ids()] """Roles that appear in skill-binding UI/maps (specialists only; no manager)."""
return ordered_specialist_ids()
def skill_role_binding_enabled_env_present() -> bool: def skill_role_binding_enabled_env_present() -> bool:
@ -80,24 +82,33 @@ def normalize_skill_role_binding(
roles = available_roles if available_roles is not None else ordered_binding_roles() roles = available_roles if available_roles is not None else ordered_binding_roles()
role_set = set(roles) role_set = set(roles)
out: dict[str, list[str]] = {r: [] for r in roles} out: dict[str, list[str]] = {r: [] for r in roles}
for k, v in (mapping_raw or {}).items():
rk = str(k or "").strip().lower() def _append(rk: str, items: Any) -> None:
if rk not in role_set: if rk not in role_set:
continue return
items = v if isinstance(v, list) else [] rows = items if isinstance(items, list) else []
seen: set[str] = set() seen = set(out[rk])
for x in items: for x in rows:
nm = str(x or "").strip() nm = str(x or "").strip()
if not nm or nm not in valid_skill_names or nm in seen: if not nm or nm not in valid_skill_names or nm in seen:
continue continue
seen.add(nm) seen.add(nm)
out[rk].append(nm) out[rk].append(nm)
for k, v in (mapping_raw or {}).items():
rk = str(k or "").strip().lower()
# Legacy Manager bindings fold into generalist.
if rk == "manager":
rk = "generalist"
_append(rk, v)
return out return out
def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]: def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]:
"""Union of manager-bound skills and skills bound to the given specialist role.""" """Skills bound to the given specialist role, plus public workspace skills."""
r = str(role or "").strip().lower() r = str(role or "").strip().lower()
if r == "manager":
r = "generalist"
if not r: if not r:
return set() return set()
from runtime.skills import discover_public_workspace_skill_names from runtime.skills import discover_public_workspace_skill_names
@ -107,18 +118,8 @@ def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]
mapping_raw=load_skill_role_binding_dict(store), mapping_raw=load_skill_role_binding_dict(store),
valid_skill_names=_all_installed_skill_names(store), valid_skill_names=_all_installed_skill_names(store),
) )
try:
raw_env = str(os.getenv(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
if raw_env:
inherit_mgr = _truthy(raw_env)
else:
raw = str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
inherit_mgr = _truthy(raw) if raw else True
except Exception:
inherit_mgr = True
mgr = {str(x).strip() for x in (mapping.get("manager") or []) if str(x).strip()} if inherit_mgr else set()
sp = {str(x).strip() for x in (mapping.get(r) or []) if str(x).strip()} sp = {str(x).strip() for x in (mapping.get(r) or []) if str(x).strip()}
return mgr | sp | public return sp | public
def _all_installed_skill_names(store: Any) -> set[str]: def _all_installed_skill_names(store: Any) -> set[str]:

View file

@ -85,7 +85,6 @@ def _executor_prompt_settings_signature(store: Any) -> tuple[str, ...]:
"AIA_SKILLS_PROMPT_IN_SYSTEM", "AIA_SKILLS_PROMPT_IN_SYSTEM",
"AIA_SKILL_DISABLED_NAMES", "AIA_SKILL_DISABLED_NAMES",
"AIA_SKILL_ROLE_BINDING_ENABLED", "AIA_SKILL_ROLE_BINDING_ENABLED",
"AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT",
SKILL_ROLE_BINDING_KEY, SKILL_ROLE_BINDING_KEY,
"AIA_PROJECT_CONTEXT_MAX_FILE_CHARS", "AIA_PROJECT_CONTEXT_MAX_FILE_CHARS",
"AIA_PROJECT_CONTEXT_MAX_TOTAL_CHARS", "AIA_PROJECT_CONTEXT_MAX_TOTAL_CHARS",

View file

@ -230,10 +230,8 @@ def materialize_mcp_tools_for_specialist(
return "mcp-remote" in joined and "/api/v1/mcps/webparser/sse" in joined return "mcp-remote" in joined and "/api/v1/mcps/webparser/sse" in joined
sp = str(specialist or "").strip().lower() sp = str(specialist or "").strip().lower()
if sp == "manager": if sp in {"manager", "manager_self", "main"}:
# Manager is a first-class binding role in admin UI/config. sp = "generalist"
# We keep it separate from generalist instead of aliasing.
sp = "manager"
# Preferred mapping: specialist -> server_ids # Preferred mapping: specialist -> server_ids
binding_server_ids: set[str] | None = None binding_server_ids: set[str] | None = None
try: try:
@ -243,6 +241,9 @@ def materialize_mcp_tools_for_specialist(
obj = json.loads(raw_binding) obj = json.loads(raw_binding)
if isinstance(obj, dict): if isinstance(obj, dict):
rows = obj.get(sp) rows = obj.get(sp)
# Legacy: if this specialist has no key, try manager bindings for generalist.
if rows is None and sp == "generalist" and "manager" in obj:
rows = obj.get("manager")
# 缺键或 null:视为未配置该专家的绑定 → 走下方「仅 coarse allowlist」逻辑(可见全部已启用 MCP)。 # 缺键或 null:视为未配置该专家的绑定 → 走下方「仅 coarse allowlist」逻辑(可见全部已启用 MCP)。
# 仅当键存在且为 JSON 数组时,才按白名单过滤(含空数组 = 刻意不给该专家任何 MCP)。 # 仅当键存在且为 JSON 数组时,才按白名单过滤(含空数组 = 刻意不给该专家任何 MCP)。
if rows is None: if rows is None:
@ -262,8 +263,12 @@ def materialize_mcp_tools_for_specialist(
except Exception: except Exception:
raw_allowed = "" raw_allowed = ""
if not raw_allowed: if not raw_allowed:
raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,manager,ops").strip() raw_allowed = str(os.getenv("AIA_MCP_SPECIALISTS") or "generalist,ops").strip()
allowed = {x.strip().lower() for x in raw_allowed.split(",") if x.strip()} allowed = {x.strip().lower() for x in raw_allowed.split(",") if x.strip()}
allowed.discard("manager")
allowed.discard("manager_self")
if "generalist" not in allowed and "ops" in allowed:
pass
if binding_server_ids is None and sp and sp not in allowed: if binding_server_ids is None and sp and sp not in allowed:
return [] return []
out: list[ToolSpec] = [] out: list[ToolSpec] = []

View file

@ -118,12 +118,12 @@ class AdminSkillsApiTests(unittest.TestCase):
self.assertIn("available_roles", body) self.assertIn("available_roles", body)
self.assertIn("mapping", body) self.assertIn("mapping", body)
roles = list(body.get("available_roles") or []) roles = list(body.get("available_roles") or [])
self.assertIn("manager", roles) self.assertNotIn("manager", roles)
self.assertIn("generalist", roles)
mapping = dict(body.get("mapping") or {}) mapping = dict(body.get("mapping") or {})
for r in roles: for r in roles:
mapping.setdefault(r, []) mapping.setdefault(r, [])
mapping["generalist"] = ["bind_demo_skill"] mapping["generalist"] = ["bind_demo_skill"]
mapping["manager"] = ["bind_demo_skill"]
s = self.client.post( s = self.client.post(
"/admin/api/skills/binding", "/admin/api/skills/binding",
json={"enabled": True, "mapping": mapping}, json={"enabled": True, "mapping": mapping},
@ -167,10 +167,8 @@ class AdminSkillsApiTests(unittest.TestCase):
json={ json={
"enabled": True, "enabled": True,
"mapping": { "mapping": {
"manager": ["effective_demo_skill"], "generalist": ["effective_demo_skill"],
"generalist": [],
"ops": [], "ops": [],
"image": [],
"memory": [], "memory": [],
}, },
}, },
@ -182,12 +180,12 @@ class AdminSkillsApiTests(unittest.TestCase):
body = r.json() or {} body = r.json() or {}
self.assertTrue(body.get("ok")) self.assertTrue(body.get("ok"))
items = list(body.get("items") or []) items = list(body.get("items") or [])
self.assertTrue(any(str(x.get("role") or "") == "manager" for x in items)) self.assertFalse(any(str(x.get("role") or "") == "manager" for x in items))
mgr = next((x for x in items if str(x.get("role") or "") == "manager"), {}) gen = next((x for x in items if str(x.get("role") or "") == "generalist"), {})
self.assertGreaterEqual(int(mgr.get("workspace_total") or 0), 1) self.assertGreaterEqual(int(gen.get("workspace_total") or 0), 1)
self.assertGreaterEqual(int(mgr.get("total") or 0), int(mgr.get("workspace_total") or 0)) self.assertGreaterEqual(int(gen.get("total") or 0), int(gen.get("workspace_total") or 0))
self.assertIn("workspace_docs_only", mgr) self.assertIn("workspace_docs_only", gen)
self.assertIn("workspace_resolved_tool_match", mgr) self.assertIn("workspace_resolved_tool_match", gen)
def test_skills_retry_install_registry(self) -> None: def test_skills_retry_install_registry(self) -> None:
pkg = Path(self._tmp.name) / "pkg2" pkg = Path(self._tmp.name) / "pkg2"

View file

@ -13,15 +13,16 @@ def _set_project_root(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
def test_build_role_system_context_reads_runtime_workspaces(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def test_build_role_system_context_reads_runtime_workspaces(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_set_project_root(monkeypatch, tmp_path) _set_project_root(monkeypatch, tmp_path)
ws = tmp_path / "runtime" / "workspaces" / "main" ws = tmp_path / "runtime" / "workspaces" / "generalist"
ws.mkdir(parents=True, exist_ok=True) ws.mkdir(parents=True, exist_ok=True)
(ws / "SOUL.md").write_text("main soul", encoding="utf-8") (ws / "SOUL.md").write_text("generalist soul", encoding="utf-8")
(ws / "ROLE_SYSTEM.md").write_text("main role system", encoding="utf-8") (ws / "ROLE_SYSTEM.md").write_text("generalist role system", encoding="utf-8")
# Legacy manager/main aliases resolve to the generalist workspace.
out = loader_mod.build_role_system_context("manager") out = loader_mod.build_role_system_context("manager")
assert "# SOUL" in out assert "# SOUL" in out
assert "main soul" in out assert "generalist soul" in out
assert "# ROLE_SYSTEM" in out assert "# ROLE_SYSTEM" in out
assert "main role system" in out assert "generalist role system" in out
def test_build_role_system_context_cache_invalidates_on_file_change( def test_build_role_system_context_cache_invalidates_on_file_change(

View file

@ -114,14 +114,22 @@ class McpAdapterTests(unittest.TestCase):
server_id="echo-b", server_id="echo-b",
tools=[{"tool_name": "ping_b", "description": "Ping B", "parameters": {"type": "object", "properties": {}}}], tools=[{"tool_name": "ping_b", "description": "Ping B", "parameters": {"type": "object", "properties": {}}}],
) )
# Legacy "manager" alias resolves to generalist; when generalist has its own
# binding, that wins (manager key is not merged).
store.set_setting("mcp_specialist_server_binding", '{"manager":["echo-a"],"generalist":["echo-b"]}') store.set_setting("mcp_specialist_server_binding", '{"manager":["echo-a"],"generalist":["echo-b"]}')
m_specs = materialize_mcp_tools_for_specialist(store, specialist="manager") m_specs = materialize_mcp_tools_for_specialist(store, specialist="manager")
g_specs = materialize_mcp_tools_for_specialist(store, specialist="generalist") g_specs = materialize_mcp_tools_for_specialist(store, specialist="generalist")
m_names = {x.name for x in m_specs} m_names = {x.name for x in m_specs}
g_names = {x.name for x in g_specs} g_names = {x.name for x in g_specs}
self.assertIn("mcp__echo-a__ping_a", m_names) self.assertIn("mcp__echo-b__ping_b", m_names)
self.assertNotIn("mcp__echo-b__ping_b", m_names) self.assertNotIn("mcp__echo-a__ping_a", m_names)
self.assertIn("mcp__echo-b__ping_b", g_names) self.assertIn("mcp__echo-b__ping_b", g_names)
# When only manager key exists, generalist falls back to that binding.
store.set_setting("mcp_specialist_server_binding", '{"manager":["echo-a"]}')
g2 = materialize_mcp_tools_for_specialist(store, specialist="generalist")
g2_names = {x.name for x in g2}
self.assertIn("mcp__echo-a__ping_a", g2_names)
self.assertNotIn("mcp__echo-b__ping_b", g2_names)
def test_binding_empty_json_object_falls_back_to_all_mcp_for_specialist(self) -> None: def test_binding_empty_json_object_falls_back_to_all_mcp_for_specialist(self) -> None:
"""{} 不应把每个专家都当成「已绑定但列表为空」而屏蔽全部 MCP。""" """{} 不应把每个专家都当成「已绑定但列表为空」而屏蔽全部 MCP。"""

View file

@ -13,22 +13,8 @@ class _DummyStore:
return str(self._settings.get(key, "")) return str(self._settings.get(key, ""))
def test_get_manager_prompt_prebuild_includes_structured_skills(monkeypatch) -> None: def test_get_manager_prompt_prebuild_is_legacy_stub(monkeypatch) -> None:
monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops", "memory", "image")) monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops", "memory", "image"))
monkeypatch.setattr(
pp,
"list_experts",
lambda: [
{"id": "generalist", "files": {"ROLE_SYSTEM.md": "General specialist for broad tasks."}},
{"id": "ops", "files": {"ROLE_SYSTEM.md": "Ops specialist for runtime and services."}},
],
)
monkeypatch.setattr(pp, "expert_workspace_signature_token", lambda: ("sig",))
def _ctx(_role: str, template_vars: dict[str, Any] | None = None) -> str:
return f"CTX\n{str((template_vars or {}).get('MANAGER_DYNAMIC_EXPERTS_HINT') or '')}"
monkeypatch.setattr(pp, "build_role_system_context", _ctx)
out = pp.get_manager_prompt_prebuild( out = pp.get_manager_prompt_prebuild(
store=_DummyStore(), store=_DummyStore(),
@ -37,20 +23,11 @@ def test_get_manager_prompt_prebuild_includes_structured_skills(monkeypatch) ->
memory_enabled=True, memory_enabled=True,
) )
assert "generalist" in str(out.get("allowed_fixed") or "") assert "generalist" in str(out.get("allowed_fixed") or "")
assert "- generalist:" in str(out.get("manager_context") or "") assert "ops" in str(out.get("allowed_fixed") or "")
assert "General specialist for broad tasks." in str(out.get("manager_context") or "") assert str(out.get("manager_context") or "") == ""
def test_warm_startup_prompt_prebuild_warms_all_roles(monkeypatch) -> None: def test_warm_startup_prompt_prebuild_warms_specialists_only(monkeypatch) -> None:
monkeypatch.setattr(
pp,
"get_manager_prompt_prebuild",
lambda **_k: {
"manager_context": "manager_ctx",
"allowed_fixed": ("generalist", "ops"),
"allowed_fixed_quoted": '"generalist", "ops"',
},
)
monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops")) monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops"))
monkeypatch.setattr(pp, "build_role_system_context", lambda role, template_vars=None: f"{role}_ctx") monkeypatch.setattr(pp, "build_role_system_context", lambda role, template_vars=None: f"{role}_ctx")
@ -71,20 +48,13 @@ def test_warm_startup_prompt_prebuild_warms_all_roles(monkeypatch) -> None:
assert out["ok"] is True assert out["ok"] is True
role_map = captured.get("role_base_systems") if isinstance(captured, dict) else {} role_map = captured.get("role_base_systems") if isinstance(captured, dict) else {}
assert isinstance(role_map, dict) assert isinstance(role_map, dict)
assert "manager" in role_map assert "manager" not in role_map
assert "generalist" in role_map assert "generalist" in role_map
assert "ops" in role_map assert "ops" in role_map
def test_runtime_prewarm_prompts_snapshot_returns_roles(monkeypatch) -> None: def test_runtime_prewarm_prompts_snapshot_returns_roles(monkeypatch) -> None:
monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops")) monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops"))
monkeypatch.setattr(
pp,
"get_manager_prompt_prebuild",
lambda **_k: {
"manager_context": "manager_ctx",
},
)
monkeypatch.setattr(pp, "build_role_system_context", lambda role, template_vars=None: f"{role}_ctx") monkeypatch.setattr(pp, "build_role_system_context", lambda role, template_vars=None: f"{role}_ctx")
monkeypatch.setattr(pp, "get_executor_prompt_static", lambda **kwargs: f"exec::{kwargs.get('skill_binding_role')}") monkeypatch.setattr(pp, "get_executor_prompt_static", lambda **kwargs: f"exec::{kwargs.get('skill_binding_role')}")
monkeypatch.setattr(pp, "default_registry", lambda **kwargs: object()) monkeypatch.setattr(pp, "default_registry", lambda **kwargs: object())
@ -92,54 +62,32 @@ def test_runtime_prewarm_prompts_snapshot_returns_roles(monkeypatch) -> None:
out = pp.runtime_prewarm_prompts_snapshot(store=_DummyStore()) out = pp.runtime_prewarm_prompts_snapshot(store=_DummyStore())
assert out["ok"] is True assert out["ok"] is True
prompts = out.get("prompts") or {} prompts = out.get("prompts") or {}
assert "manager" in prompts assert "manager" not in prompts
assert "generalist" in prompts assert "generalist" in prompts
assert "ops" in prompts assert "ops" in prompts
assert prompts["manager"].get("system_prompt") == "exec::manager"
assert prompts["generalist"].get("system_prompt") == "exec::generalist" assert prompts["generalist"].get("system_prompt") == "exec::generalist"
assert prompts["ops"].get("system_prompt") == "exec::ops" assert prompts["ops"].get("system_prompt") == "exec::ops"
assert "manager_system_prompt" not in prompts["manager"]
assert "executor_system_prompt" not in prompts["manager"] aliased = pp.runtime_prewarm_prompts_snapshot(store=_DummyStore(), role="manager")
assert "manager_user_scaffold" not in prompts["manager"] assert aliased["ok"] is True
assert aliased.get("roles") == ["generalist"]
assert "generalist" in (aliased.get("prompts") or {})
def test_manager_prompt_prebuild_cache_invalidates_on_workspace_revision_change(monkeypatch) -> None: def test_manager_prompt_prebuild_stub_is_stateless(monkeypatch) -> None:
token = {"v": 1} monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops", "memory"))
calls = {"ctx": 0} a = pp.get_manager_prompt_prebuild(
monkeypatch.setattr(pp, "discover_specialist_ids", lambda: ("generalist", "ops"))
monkeypatch.setattr(
pp,
"list_experts",
lambda: [{"id": "generalist", "files": {"ROLE_SYSTEM.md": "General specialist"}}],
)
monkeypatch.setattr(pp, "expert_workspace_signature_token", lambda: ("revision", token["v"]))
def _ctx(_role: str, template_vars: dict[str, Any] | None = None) -> str:
calls["ctx"] += 1
return f"CTX\n{str((template_vars or {}).get('MANAGER_DYNAMIC_EXPERTS_HINT') or '')}"
monkeypatch.setattr(pp, "build_role_system_context", _ctx)
_ = pp.get_manager_prompt_prebuild(
store=_DummyStore(), store=_DummyStore(),
registry=object(), registry=object(),
base_url="", base_url="",
memory_enabled=True, memory_enabled=True,
) )
_ = pp.get_manager_prompt_prebuild( b = pp.get_manager_prompt_prebuild(
store=_DummyStore(), store=_DummyStore(),
registry=object(), registry=object(),
base_url="", base_url="",
memory_enabled=True, memory_enabled=False,
) )
assert calls["ctx"] == 1 assert a.get("manager_context") == ""
assert "memory" not in (b.get("allowed_fixed") or ())
token["v"] = 2 assert "memory" in (a.get("allowed_fixed") or ())
_ = pp.get_manager_prompt_prebuild(
store=_DummyStore(),
registry=object(),
base_url="",
memory_enabled=True,
)
assert calls["ctx"] == 2

View file

@ -40,6 +40,7 @@ def test_collect_respects_role_binding_union(tmp_path: Path, monkeypatch) -> Non
store.set_setting(SKILL_ROLE_BINDING_ENABLED_SETTING, "1") store.set_setting(SKILL_ROLE_BINDING_ENABLED_SETTING, "1")
mapping = {r: [] for r in ordered_binding_roles()} mapping = {r: [] for r in ordered_binding_roles()}
mapping["generalist"] = ["skill-alpha"] mapping["generalist"] = ["skill-alpha"]
# Legacy manager bindings fold into generalist.
mapping["manager"] = ["skill-beta"] mapping["manager"] = ["skill-beta"]
store.set_setting(SKILL_ROLE_BINDING_KEY, json.dumps(mapping)) store.set_setting(SKILL_ROLE_BINDING_KEY, json.dumps(mapping))
@ -141,14 +142,15 @@ def test_collect_unfiltered_when_binding_disabled(tmp_path: Path, monkeypatch) -
def test_normalize_drops_unknown_skills(tmp_path: Path) -> None: def test_normalize_drops_unknown_skills(tmp_path: Path) -> None:
roles = ["manager", "generalist"] roles = ["generalist", "ops"]
out = normalize_skill_role_binding( out = normalize_skill_role_binding(
mapping_raw={"manager": ["nope", "skill-x"], "generalist": ["skill-x"]}, mapping_raw={"manager": ["nope", "skill-x"], "generalist": ["skill-x"], "ops": ["nope"]},
valid_skill_names={"skill-x"}, valid_skill_names={"skill-x"},
available_roles=roles, available_roles=roles,
) )
assert out["manager"] == ["skill-x"] assert "manager" not in out
assert out["generalist"] == ["skill-x"] assert out["generalist"] == ["skill-x"]
assert out["ops"] == []
def test_skill_role_binding_env_overrides_store_value(tmp_path: Path, monkeypatch) -> None: def test_skill_role_binding_env_overrides_store_value(tmp_path: Path, monkeypatch) -> None:

View file

@ -11,7 +11,8 @@ def test_unknown_dynamic_specialist_defaults_to_minimum_expert_permissions(monke
def test_agent_role_ids_uses_runtime_discovery(monkeypatch) -> None: def test_agent_role_ids_uses_runtime_discovery(monkeypatch) -> None:
monkeypatch.setattr(specialists_mod, "discover_specialist_ids", lambda: ("generalist", "ops", "qa")) monkeypatch.setattr(specialists_mod, "discover_specialist_ids", lambda: ("generalist", "ops", "qa"))
got = specialists_mod.agent_role_ids() got = specialists_mod.agent_role_ids()
assert got[0] == specialists_mod.MANAGER_AGENT_ID assert got[0] == "generalist"
assert "manager" not in set(got)
assert "qa" in set(got) assert "qa" in set(got)

View file

@ -15,7 +15,7 @@ def test_tool_wire_freeze_default_on(monkeypatch) -> None:
monkeypatch.setattr(dl, "_prepare_llm_tools", lambda **kwargs: []) monkeypatch.setattr(dl, "_prepare_llm_tools", lambda **kwargs: [])
monkeypatch.setenv("AIA_TOOL_WIRE_FROZEN_ON_STARTUP", "") monkeypatch.setenv("AIA_TOOL_WIRE_FROZEN_ON_STARTUP", "")
store = _DummyStore() store = _DummyStore()
_ = dl.warm_tool_wire_cache(store=store, tools=object(), base_url="", roles=["manager"]) _ = dl.warm_tool_wire_cache(store=store, tools=object(), base_url="", roles=["generalist"])
st = dl.tool_wire_freeze_status(store=store) st = dl.tool_wire_freeze_status(store=store)
assert st["enabled"] is True assert st["enabled"] is True
assert st["frozen"] is True assert st["frozen"] is True
@ -24,7 +24,7 @@ def test_tool_wire_freeze_default_on(monkeypatch) -> None:
def test_tool_wire_freeze_disabled_by_setting(monkeypatch) -> None: def test_tool_wire_freeze_disabled_by_setting(monkeypatch) -> None:
monkeypatch.setattr(dl, "_prepare_llm_tools", lambda **kwargs: []) monkeypatch.setattr(dl, "_prepare_llm_tools", lambda **kwargs: [])
store = _DummyStore({"AIA_TOOL_WIRE_FROZEN_ON_STARTUP": "0"}) store = _DummyStore({"AIA_TOOL_WIRE_FROZEN_ON_STARTUP": "0"})
_ = dl.warm_tool_wire_cache(store=store, tools=object(), base_url="", roles=["manager"]) _ = dl.warm_tool_wire_cache(store=store, tools=object(), base_url="", roles=["generalist"])
st = dl.tool_wire_freeze_status(store=store) st = dl.tool_wire_freeze_status(store=store)
assert st["enabled"] is False assert st["enabled"] is False
assert st["frozen"] is False assert st["frozen"] is False