feat(gateway): share_auth on profiles.create + MCP servers in describe/configure (#85963)

Three widenings for capabilities UIs (Bot Mode's bot builder):

1. profiles.create share_auth (default false): skip the auth.json
   COPY so the new profile reads OAuth/token state through the
   existing global-root fallback and refreshes write through to it.
   A copy forks token state — the first refresh on either side
   invalidates the other for single-use refresh tokens; sharing keeps
   ONE live token pool for the main profile and every bot. Static
   .env keys still copy (no refresh semantics). Receipt:
   mirrored.auth = 'shared'.

2. profiles.describe reports mcp_servers
   [{name, enabled, transport}] from the profile's config.

3. profiles.configure accepts enabled_mcp_servers (replace
   semantics): toggles via the standard disabled flag; enabling a
   server the profile lacks copies its definition from the launch
   profile's catalog (names never invented). Launch catalog read
   BEFORE the home override flips config resolution.

E2E: describe keys include mcp_servers; create with share_auth ->
mirrored.auth='shared' + no auth.json in the profile dir; configure
applied.mcp_servers=true.
This commit is contained in:
Teknium
2026-08-14 02:08:16 -07:00
committed by GitHub
parent 16b54e2a0f
commit 4bbc6f258a
+94 -3
View File
@@ -246,7 +246,19 @@ def _(rid, params: dict) -> dict:
# .env (only over the seeded comment-only stub — never clobber real
# secrets a clone brought along) and auth.json (only when absent), then
# inherit model.provider/model.default unless the caller pinned a model.
#
# ``share_auth`` (default false): SKIP the auth.json copy so the new
# profile reads OAuth/token state through the global-root fallback
# instead (hermes_cli.auth: profile reads fall back to the global
# store, and token refreshes write THROUGH to it). A copy forks token
# state — the first refresh in either store invalidates the other
# for single-use refresh tokens. Sharing keeps one live token pool
# for the main profile and every bot. Static .env keys still copy
# (no refresh semantics, so copying is safe).
mirrored = {"env": False, "auth": False, "model_inherited": False, "voice": False}
share_auth = is_truthy_value(params.get("share_auth", False))
if share_auth:
mirrored["auth"] = "shared"
if is_truthy_value(params.get("mirror_credentials", True)):
import shutil
@@ -268,7 +280,7 @@ def _(rid, params: dict) -> dict:
try:
src_auth = launch_home / "auth.json"
dst_auth = path / "auth.json"
if src_auth.is_file() and not dst_auth.exists():
if not share_auth and src_auth.is_file() and not dst_auth.exists():
shutil.copy2(src_auth, dst_auth)
try:
os.chmod(str(dst_auth), 0o600)
@@ -447,6 +459,31 @@ def _(rid, params: dict) -> dict:
except Exception:
pass
# MCP servers configured for this profile (config.yaml
# mcp_servers). Report name + enabled + a transport hint so a
# capabilities UI can list and toggle them without parsing the
# raw config shape.
mcp_out = []
try:
mcp_cfg = cfg.get("mcp_servers")
if isinstance(mcp_cfg, dict):
for srv_name in sorted(mcp_cfg.keys()):
entry = mcp_cfg.get(srv_name)
if not isinstance(entry, dict):
continue
transport = "stdio"
if entry.get("url"):
transport = str(entry.get("transport") or "http")
mcp_out.append(
{
"name": str(srv_name),
"enabled": not is_truthy_value(entry.get("disabled", False)),
"transport": transport,
}
)
except Exception:
pass
model_cfg = cfg.get("model") if isinstance(cfg.get("model"), dict) else {}
description = ""
@@ -470,6 +507,7 @@ def _(rid, params: dict) -> dict:
"skills": installed,
"toolsets": toolsets_out,
"toolsets_pinned": pinned_set is not None,
"mcp_servers": mcp_out,
},
)
finally:
@@ -582,10 +620,25 @@ def _(rid, params: dict) -> dict:
except Exception:
applied["model"] = False
needs_cfg = isinstance(params.get("disabled_skills"), list) or isinstance(
params.get("enabled_toolsets"), list
needs_cfg = (
isinstance(params.get("disabled_skills"), list)
or isinstance(params.get("enabled_toolsets"), list)
or isinstance(params.get("enabled_mcp_servers"), list)
)
if needs_cfg:
# Launch profile's MCP catalog, read BEFORE the home override
# flips config resolution to the target profile.
launch_mcp = {}
if isinstance(params.get("enabled_mcp_servers"), list):
try:
from hermes_cli.config import load_config_readonly
launch_cfg = load_config_readonly() or {}
if isinstance(launch_cfg.get("mcp_servers"), dict):
launch_mcp = launch_cfg["mcp_servers"]
except Exception:
launch_mcp = {}
token = set_hermes_home_override(str(profile_dir))
try:
from hermes_cli.config import load_config, save_config
@@ -620,6 +673,44 @@ def _(rid, params: dict) -> dict:
applied["toolsets"] = True
except Exception:
applied["toolsets"] = False
# ``enabled_mcp_servers`` (list[str], replace semantics):
# toggle the profile's mcp_servers entries via the standard
# ``disabled`` flag. Enabling a server the profile doesn't
# define copies its definition from the LAUNCH profile's
# config (capabilities UIs offer the main profile's catalog);
# unknown names are skipped, never invented. Server defs are
# config, not secrets — credentials stay in .env/auth.
if isinstance(params.get("enabled_mcp_servers"), list):
try:
wanted = {
str(s).strip()
for s in params["enabled_mcp_servers"]
if str(s).strip()
}
cfg = load_config() or {}
mcp_cfg = (
cfg.get("mcp_servers")
if isinstance(cfg.get("mcp_servers"), dict)
else {}
)
for srv in wanted:
if srv in mcp_cfg and isinstance(mcp_cfg[srv], dict):
mcp_cfg[srv].pop("disabled", None)
elif srv in launch_mcp and isinstance(launch_mcp[srv], dict):
mcp_cfg[srv] = dict(launch_mcp[srv])
mcp_cfg[srv].pop("disabled", None)
for srv, entry in mcp_cfg.items():
if srv not in wanted and isinstance(entry, dict):
entry["disabled"] = True
if mcp_cfg:
cfg["mcp_servers"] = mcp_cfg
save_config(cfg)
applied["mcp_servers"] = True
except Exception:
applied["mcp_servers"] = False
finally:
reset_hermes_home_override(token)