fix(dashboard): persist Discord toolsets to Discord platform
parent
c80b244b52
commit
3ffd8b3da0
|
|
@ -165,6 +165,20 @@ def _toolset_allowed_for_platform(ts_key: str, platform: str) -> bool:
|
||||||
return allowed is None or platform in allowed
|
return allowed is None or platform in allowed
|
||||||
|
|
||||||
|
|
||||||
|
def _toolset_configuration_platform(ts_key: str, default: str = "cli") -> str:
|
||||||
|
"""Return the platform a platform-less configuration UI should target.
|
||||||
|
|
||||||
|
Most configurable toolsets retain the historical desktop/CLI target. A
|
||||||
|
toolset restricted away from that platform must instead be configured on
|
||||||
|
one of its supported platforms; otherwise the shared save helper correctly
|
||||||
|
drops it and the UI reports a successful no-op.
|
||||||
|
"""
|
||||||
|
allowed = _TOOLSET_PLATFORM_RESTRICTIONS.get(ts_key)
|
||||||
|
if not allowed or default in allowed:
|
||||||
|
return default
|
||||||
|
return sorted(allowed)[0]
|
||||||
|
|
||||||
|
|
||||||
def _get_effective_configurable_toolsets():
|
def _get_effective_configurable_toolsets():
|
||||||
"""Return CONFIGURABLE_TOOLSETS + any plugin-provided toolsets.
|
"""Return CONFIGURABLE_TOOLSETS + any plugin-provided toolsets.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13700,29 +13700,43 @@ async def get_toolsets(profile: Optional[str] = None):
|
||||||
from hermes_cli.tools_config import (
|
from hermes_cli.tools_config import (
|
||||||
_get_effective_configurable_toolsets,
|
_get_effective_configurable_toolsets,
|
||||||
_get_platform_tools,
|
_get_platform_tools,
|
||||||
|
_toolset_configuration_platform,
|
||||||
_toolset_has_keys,
|
_toolset_has_keys,
|
||||||
gui_toolset_label,
|
gui_toolset_label,
|
||||||
)
|
)
|
||||||
|
from hermes_cli.platforms import platform_label
|
||||||
from toolsets import resolve_toolset
|
from toolsets import resolve_toolset
|
||||||
|
|
||||||
with _profile_scope(profile):
|
with _profile_scope(profile):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
enabled_toolsets = _get_platform_tools(
|
toolset_rows = _get_effective_configurable_toolsets()
|
||||||
config,
|
target_platforms = {
|
||||||
"cli",
|
_toolset_configuration_platform(name) for name, _, _ in toolset_rows
|
||||||
include_default_mcp_servers=False,
|
}
|
||||||
)
|
enabled_by_platform = {
|
||||||
|
platform: _get_platform_tools(
|
||||||
|
config,
|
||||||
|
platform,
|
||||||
|
include_default_mcp_servers=False,
|
||||||
|
)
|
||||||
|
for platform in target_platforms
|
||||||
|
}
|
||||||
result = []
|
result = []
|
||||||
for name, label, desc in _get_effective_configurable_toolsets():
|
for name, label, desc in toolset_rows:
|
||||||
try:
|
try:
|
||||||
tools = sorted(set(resolve_toolset(name)))
|
tools = sorted(set(resolve_toolset(name)))
|
||||||
except Exception:
|
except Exception:
|
||||||
tools = []
|
tools = []
|
||||||
is_enabled = name in enabled_toolsets
|
target_platform = _toolset_configuration_platform(name)
|
||||||
|
is_enabled = name in enabled_by_platform[target_platform]
|
||||||
result.append({
|
result.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"label": gui_toolset_label(label),
|
"label": gui_toolset_label(label),
|
||||||
"description": desc,
|
"description": desc,
|
||||||
|
"platform": target_platform,
|
||||||
|
"platform_label": gui_toolset_label(
|
||||||
|
platform_label(target_platform, target_platform)
|
||||||
|
),
|
||||||
"enabled": is_enabled,
|
"enabled": is_enabled,
|
||||||
"available": is_enabled,
|
"available": is_enabled,
|
||||||
"configured": _toolset_has_keys(name, config),
|
"configured": _toolset_has_keys(name, config),
|
||||||
|
|
@ -13738,34 +13752,46 @@ class ToolsetToggle(BaseModel):
|
||||||
|
|
||||||
@app.put("/api/tools/toolsets/{name}")
|
@app.put("/api/tools/toolsets/{name}")
|
||||||
async def toggle_toolset(name: str, body: ToolsetToggle, profile: Optional[str] = None):
|
async def toggle_toolset(name: str, body: ToolsetToggle, profile: Optional[str] = None):
|
||||||
"""Enable/disable a configurable toolset for the desktop (cli) platform.
|
"""Enable/disable a configurable toolset for its configuration platform.
|
||||||
|
|
||||||
Persists to ``platform_toolsets.cli`` via the same ``_save_platform_tools``
|
Most toolsets persist to ``platform_toolsets.cli``. Platform-restricted
|
||||||
helper the CLI ``hermes tools`` picker uses, so the GUI and CLI stay in
|
toolsets instead target their supported platform (for example, Discord's
|
||||||
lockstep. Scoped to ``body.profile`` when provided. Returns 400 for
|
native toolsets persist to ``platform_toolsets.discord``). The shared
|
||||||
unknown toolset keys.
|
``_save_platform_tools`` helper keeps the GUI and CLI in lockstep. Scoped
|
||||||
|
to ``body.profile`` when provided. Returns 400 for unknown toolset keys.
|
||||||
"""
|
"""
|
||||||
from hermes_cli.tools_config import (
|
from hermes_cli.tools_config import (
|
||||||
_get_effective_configurable_toolsets,
|
_get_effective_configurable_toolsets,
|
||||||
_get_platform_tools,
|
_get_platform_tools,
|
||||||
_save_platform_tools,
|
_save_platform_tools,
|
||||||
|
_toolset_configuration_platform,
|
||||||
)
|
)
|
||||||
|
|
||||||
valid = {ts_key for ts_key, _, _ in _get_effective_configurable_toolsets()}
|
valid = {ts_key for ts_key, _, _ in _get_effective_configurable_toolsets()}
|
||||||
if name not in valid:
|
if name not in valid:
|
||||||
raise HTTPException(status_code=400, detail=f"Unknown toolset: {name}")
|
raise HTTPException(status_code=400, detail=f"Unknown toolset: {name}")
|
||||||
|
|
||||||
|
target_platform = _toolset_configuration_platform(name)
|
||||||
with _profile_scope(body.profile or profile):
|
with _profile_scope(body.profile or profile):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
enabled = set(
|
enabled = set(
|
||||||
_get_platform_tools(config, "cli", include_default_mcp_servers=False)
|
_get_platform_tools(
|
||||||
|
config,
|
||||||
|
target_platform,
|
||||||
|
include_default_mcp_servers=False,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if body.enabled:
|
if body.enabled:
|
||||||
enabled.add(name)
|
enabled.add(name)
|
||||||
else:
|
else:
|
||||||
enabled.discard(name)
|
enabled.discard(name)
|
||||||
_save_platform_tools(config, "cli", enabled)
|
_save_platform_tools(config, target_platform, enabled)
|
||||||
return {"ok": True, "name": name, "enabled": body.enabled}
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"name": name,
|
||||||
|
"platform": target_platform,
|
||||||
|
"enabled": body.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/tools/toolsets/{name}/config")
|
@app.get("/api/tools/toolsets/{name}/config")
|
||||||
|
|
|
||||||
|
|
@ -4792,6 +4792,8 @@ class TestNewEndpoints:
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"label": "Web Search & Scraping",
|
"label": "Web Search & Scraping",
|
||||||
"description": "web_search, web_extract",
|
"description": "web_search, web_extract",
|
||||||
|
"platform": "cli",
|
||||||
|
"platform_label": "CLI",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"available": True,
|
"available": True,
|
||||||
"configured": False,
|
"configured": False,
|
||||||
|
|
@ -4801,6 +4803,8 @@ class TestNewEndpoints:
|
||||||
"name": "skills",
|
"name": "skills",
|
||||||
"label": "Skills",
|
"label": "Skills",
|
||||||
"description": "list, view, manage",
|
"description": "list, view, manage",
|
||||||
|
"platform": "cli",
|
||||||
|
"platform_label": "CLI",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"available": True,
|
"available": True,
|
||||||
"configured": True,
|
"configured": True,
|
||||||
|
|
@ -4810,6 +4814,8 @@ class TestNewEndpoints:
|
||||||
"name": "memory",
|
"name": "memory",
|
||||||
"label": "Memory",
|
"label": "Memory",
|
||||||
"description": "persistent memory across sessions",
|
"description": "persistent memory across sessions",
|
||||||
|
"platform": "cli",
|
||||||
|
"platform_label": "CLI",
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"available": False,
|
"available": False,
|
||||||
"configured": True,
|
"configured": True,
|
||||||
|
|
@ -4838,6 +4844,41 @@ class TestNewEndpoints:
|
||||||
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||||
assert listing["x_search"]["enabled"] is False
|
assert listing["x_search"]["enabled"] is False
|
||||||
|
|
||||||
|
def test_discord_toolsets_read_and_write_discord_platform(self):
|
||||||
|
"""Platform-restricted toolsets must not be saved as successful CLI no-ops."""
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
|
||||||
|
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||||
|
assert listing["discord"]["platform"] == "discord"
|
||||||
|
assert listing["discord"]["platform_label"] == "Discord"
|
||||||
|
assert listing["discord"]["enabled"] is False
|
||||||
|
|
||||||
|
resp = self.client.put("/api/tools/toolsets/discord", json={"enabled": True})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() == {
|
||||||
|
"ok": True,
|
||||||
|
"name": "discord",
|
||||||
|
"platform": "discord",
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
assert "discord" in config["platform_toolsets"]["discord"]
|
||||||
|
assert "discord" not in config["platform_toolsets"].get("cli", [])
|
||||||
|
|
||||||
|
listing = {t["name"]: t for t in self.client.get("/api/tools/toolsets").json()}
|
||||||
|
assert listing["discord"]["enabled"] is True
|
||||||
|
assert listing["discord_admin"]["enabled"] is False
|
||||||
|
|
||||||
|
resp = self.client.put(
|
||||||
|
"/api/tools/toolsets/discord_admin", json={"enabled": True}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
config = load_config()
|
||||||
|
assert {"discord", "discord_admin"} <= set(
|
||||||
|
config["platform_toolsets"]["discord"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_toggle_toolset_unknown_returns_400(self):
|
def test_toggle_toolset_unknown_returns_400(self):
|
||||||
resp = self.client.put(
|
resp = self.client.put(
|
||||||
"/api/tools/toolsets/not_a_real_toolset", json={"enabled": True}
|
"/api/tools/toolsets/not_a_real_toolset", json={"enabled": True}
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,7 @@ export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Pr
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelText = toolset.label?.trim() || toolset.name;
|
const labelText = toolset.label?.trim() || toolset.name;
|
||||||
|
const platformText = toolset.platform_label?.trim() || toolset.platform;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
|
|
@ -253,10 +254,12 @@ export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Pr
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onCheckedChange={(v) => void handleToggle(v)}
|
onCheckedChange={(v) => void handleToggle(v)}
|
||||||
disabled={toggling}
|
disabled={toggling}
|
||||||
aria-label="Enable toolset"
|
aria-label={`Enable toolset for ${platformText}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{enabled ? "Enabled for the agent" : "Disabled"}
|
{enabled
|
||||||
|
? `Enabled for ${platformText}`
|
||||||
|
: `Disabled for ${platformText}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -732,7 +732,7 @@ export const api = {
|
||||||
getToolsets: (profile?: string) =>
|
getToolsets: (profile?: string) =>
|
||||||
fetchJSON<ToolsetInfo[]>(`/api/tools/toolsets${profileQuery(profile)}`),
|
fetchJSON<ToolsetInfo[]>(`/api/tools/toolsets${profileQuery(profile)}`),
|
||||||
toggleToolset: (name: string, enabled: boolean, profile?: string) =>
|
toggleToolset: (name: string, enabled: boolean, profile?: string) =>
|
||||||
fetchJSON<{ ok: boolean; name: string; enabled: boolean }>(
|
fetchJSON<{ ok: boolean; name: string; platform: string; enabled: boolean }>(
|
||||||
`/api/tools/toolsets/${encodeURIComponent(name)}`,
|
`/api/tools/toolsets/${encodeURIComponent(name)}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -2205,6 +2205,8 @@ export interface ToolsetInfo {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
platform: string;
|
||||||
|
platform_label: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
tools: string[];
|
tools: string[];
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ export default function SkillsPage() {
|
||||||
/* ---- Refresh toolsets after a config change ---- */
|
/* ---- Refresh toolsets after a config change ---- */
|
||||||
const refreshToolsets = async () => {
|
const refreshToolsets = async () => {
|
||||||
try {
|
try {
|
||||||
const tsets = await api.getToolsets();
|
const tsets = await api.getToolsets(selectedProfile || undefined);
|
||||||
setToolsets(tsets);
|
setToolsets(tsets);
|
||||||
} catch {
|
} catch {
|
||||||
/* non-fatal: the drawer already toasted on the failing write */
|
/* non-fatal: the drawer already toasted on the failing write */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue