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_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_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()
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")
specialist = normalize_requested_specialist(specialist_raw or "generalist")
specialist = _apply_specialist_flags(store, specialist)
confirm_strategy = _normalize_confirm_strategy({"confirm_strategy": (confirm_raw or "strict")})
plan_agent_version = _normalize_plan_agent_version({"plan_agent_version": (plan_agent_raw or "v1")})
return interaction_mode, specialist, confirm_strategy, plan_agent_version
# Legacy plan-mode fields kept as wire constants for older clients.
return interaction_mode, specialist, "strict", "v1"
def _persist_user_menu_chat_settings(
@ -784,6 +777,7 @@ def _persist_user_menu_chat_settings(
confirm_strategy: str,
plan_agent_version: str,
) -> None:
del confirm_strategy, plan_agent_version
store.set_setting(
_chat_user_mode_setting_key(tenant_id=tenant_id, user_id=user_id, field="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"),
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(

View file

@ -440,17 +440,15 @@ def build_admin_router() -> APIRouter:
def _ordered_specialists() -> list[str]:
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)]
def _ordered_mcp_roles() -> list[str]:
specs = _ordered_specialists()
return ["manager", *[x for x in specs if x != "manager"]]
return _ordered_specialists()
def _ordered_roles() -> list[str]:
"""Canonical role list used by Admin preview APIs."""
specs = _ordered_specialists()
return ["manager", *[x for x in specs if x != "manager"]]
return _ordered_specialists()
_EXPERT_ALLOWED_FILES = {"SOUL.md", "ROLE_SYSTEM.md"}
@ -3197,7 +3195,7 @@ def build_admin_router() -> APIRouter:
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
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_set = set(allowed)
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 []
allowset = {str(x).strip().lower() for x in items if str(x).strip().lower() in available_set}
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())
ordered = [x for x in available if x in allowset]
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 (
SKILL_ROLE_BINDING_ENABLED_SETTING,
SKILL_ROLE_BINDING_KEY,
SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING,
load_skill_role_binding_dict,
normalize_skill_role_binding,
ordered_binding_roles,
@ -348,7 +347,7 @@ def include_skill_routes(
"enabled": bool(skill_role_binding_enabled(store=store)),
"enabled_stored": bool(skill_role_binding_enabled_stored(store=store)),
"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,
"installed_skills": items,
"mapping": mapping,
@ -365,8 +364,6 @@ def include_skill_routes(
_require_tenant_write(ctx)
if "enabled" in payload:
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)
mapping_raw = payload.get("mapping") if isinstance(payload.get("mapping"), dict) else {}
mapping = normalize_skill_role_binding(
@ -388,7 +385,7 @@ def include_skill_routes(
"enabled": bool(skill_role_binding_enabled(store=store)),
"enabled_stored": bool(skill_role_binding_enabled_stored(store=store)),
"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,
"mapping": mapping,
}
@ -401,7 +398,7 @@ def include_skill_routes(
roles, mapping, _valid = _normalized_skill_binding(store)
role_rows: list[dict[str, Any]] = []
for role in roles:
specialist = "generalist" if role == "manager" else role
specialist = role
ex = build_gateway_executor(store=store, specialist=specialist)
tools = getattr(ex, "tools", None)
model = getattr(ex, "model", None)
@ -432,10 +429,8 @@ def include_skill_routes(
base_url=base_url,
)
direct_set = set(mapping.get(role) or [])
manager_set = set(mapping.get("manager") or [])
workspace_total = 0
workspace_direct = 0
workspace_inherited = 0
workspace_resolved_tool_match = 0
workspace_docs_only = 0
mcp_total = 0
@ -456,8 +451,6 @@ def include_skill_routes(
docs_only_names.append(str(nm))
if nm in direct_set:
workspace_direct += 1
elif role != "manager" and nm in manager_set:
workspace_inherited += 1
elif str(nm).startswith("mcp__"):
mcp_total += 1
else:
@ -468,7 +461,7 @@ def include_skill_routes(
"total": len(entries),
"workspace_total": workspace_total,
"workspace_direct": workspace_direct,
"workspace_inherited_manager": workspace_inherited,
"workspace_inherited_manager": 0,
"workspace_resolved_tool_match": workspace_resolved_tool_match,
"workspace_docs_only": workspace_docs_only,
"mcp_total": mcp_total,

View file

@ -1583,7 +1583,7 @@ async function renderStack() {
apiGetNoHang("/admin/api/runtime/anomalies"),
apiGetNoHang("/admin/api/runtime/scan-artifacts"),
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/channel-dispatch/weixin"),
apiGetNoHang("/admin/api/chat/settings/channel-dispatch/whatsapp"),
@ -2220,7 +2220,7 @@ async function renderStack() {
: [el("tr", {}, [el("td", { class: "muted", text: "—", colspan: "5" })])];
const promptsSectionBody = el("div");
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" }),
]);
const renderPromptCards = (resp) => {
@ -2327,7 +2327,7 @@ async function renderStack() {
]),
el("div", { class: "card" }, [
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("label", { text: currentLang === "zh" ? "角色筛选" : "Role filter" }),
promptsRoleSelect,
@ -7794,7 +7794,7 @@ async function renderSkills() {
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 skillBindingPersistHint = el("div", {
class: "muted",
@ -7807,7 +7807,6 @@ async function renderSkills() {
});
const skillEffectiveState = { items: [] };
const skillBindingEnabledCb = el("input", { type: "checkbox" });
const skillBindingManagerInheritCb = el("input", { type: "checkbox" });
const skillRoleSelect = el("select", { class: "input" }, []);
const skillBindingListWrap = el("div");
const skillBindingDashTbody = el("tbody");
@ -7816,11 +7815,9 @@ async function renderSkills() {
skillBindingDashTbody.innerHTML = "";
const roles = Array.isArray(skillBindingState.roles) ? skillBindingState.roles : [];
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) => {
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(
el("tr", {}, [
el("td", { text: role }),
@ -7860,7 +7857,6 @@ async function renderSkills() {
el("td", { text: String(x.total || 0) }),
el("td", { text: String(x.workspace_total || 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_docs_only || 0) }),
el("td", { text: String(x.mcp_total || 0) }),
@ -7871,7 +7867,7 @@ async function renderSkills() {
);
});
if (!rows.length) {
skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "11" })]));
skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "10" })]));
}
};
const loadSkillEffective = async () => {
@ -7920,9 +7916,7 @@ async function renderSkills() {
.filter(Boolean);
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled;
skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true;
skillBindingEnabledCb.checked = skillBindingState.enabled;
skillBindingManagerInheritCb.checked = skillBindingState.managerInherit;
if (r.enabled_env_present) {
skillBindingEnvHint.style.display = "block";
const stored = !!r.enabled_stored;
@ -7959,14 +7953,11 @@ async function renderSkills() {
try {
const r = await apiPost("/admin/api/skills/binding", {
enabled: !!skillBindingEnabledCb.checked,
manager_inherit: !!skillBindingManagerInheritCb.checked,
mapping: skillBindingState.mapping,
});
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
skillBindingState.enabled = !!r.enabled;
skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true;
skillBindingEnabledCb.checked = skillBindingState.enabled;
skillBindingManagerInheritCb.checked = skillBindingState.managerInherit;
if (r.enabled_env_present) {
skillBindingEnvHint.style.display = "block";
const stored = !!r.enabled_stored;
@ -7979,7 +7970,7 @@ async function renderSkills() {
skillBindingEnvHint.style.display = "none";
skillBindingEnvHint.textContent = "";
}
skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)} manager_inherit=${String(skillBindingState.managerInherit)}`;
skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)}`;
renderSkillBindingList();
renderSkillBindingDashboard();
await loadSkillEffective();
@ -7989,12 +7980,12 @@ async function renderSkills() {
},
});
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", {
class: "muted",
style: "margin:8px 0;line-height:1.5;",
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,
skillBindingPersistHint,
@ -8003,10 +7994,6 @@ async function renderSkills() {
skillBindingEnabledCb,
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("label", { text: "Role" }),
skillRoleSelect,
@ -8019,7 +8006,7 @@ async function renderSkills() {
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("table", { class: "table table--compact" }, [
el("thead", {}, [el("tr", {}, [
@ -8027,7 +8014,6 @@ async function renderSkills() {
el("th", { text: "total effective" }),
el("th", { text: "workspace" }),
el("th", { text: "direct bind" }),
el("th", { text: "inherited manager" }),
el("th", { text: "workspace resolved" }),
el("th", { text: "workspace docs-only" }),
el("th", { text: "mcp converted" }),
@ -8165,9 +8151,8 @@ async function renderSkills() {
const rolesTotal = Number(body.roles_total || 0);
const totalInternal = Number(summary.total_internal_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 : [];
selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired} permBan9999=${totalPermBan}`;
selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired}`;
selfCheckTbody.innerHTML = "";
items.forEach((x) => {
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.wired_count || 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: "wired" }),
el("th", { text: "removed mcp" }),
el("th", { text: "perm ban(9999)" }),
])]),
selfCheckTbody,
]),
@ -8888,7 +8871,7 @@ async function renderSkills() {
await loadAudits();
await loadSkillBinding();
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 = "";
rolesForPreview.forEach((role) => internalRoleSelect.appendChild(el("option", { value: role, text: role })));
internalRoleSelect.value = rolesForPreview.includes("generalist") ? "generalist" : rolesForPreview[0];