Summary
The Enterprise Studio Desktop app polls GET /enterprise/api/v1beta/config (provided by the enterprise overlay of thv serve) and extracts only state, warning, and policy from the response — ignoring the individual policy directives in the payload (playground, non_registry_servers, registry, telemetry, …).
As a result, when an administrator pushes a directive like playground: { value: false, enforcement: enforced } via the enterprise config server, the directive is signed into envelopes, propagated correctly to the Desktop's thv serve subprocess, and visible in the API response — but the Desktop UI takes no action. The Playground tab remains visible.
The permission keys, the gate, and the React data flow are all wired — only the mapping from directive value to permission state is missing.
Repro
- Deploy an enterprise-manager / config-server with a
playground directive:
enterprise-manager:
enterpriseConfig:
playground:
value: false
enforcement: "enforced"
- Sign in to ToolHive Studio Enterprise; let it fetch a fresh envelope.
- Inspect the live API response:
DESKTOP_PORT=$(ps -ef | grep -v grep | grep "ToolHiveStudioEnterprise.*thv serve" | head -1 \
| grep -oE -- "--port=[0-9]+" | grep -oE "[0-9]+")
curl -s http://127.0.0.1:$DESKTOP_PORT/enterprise/api/v1beta/config | jq
Output (truncated):
{
"playground": { "value": false, "enforcement": "enforced" },
"state": "online"
}
- Observe the Desktop UI — Playground tab still visible in the left navigation.
Expected
When playground.value === false is present in the envelope, the Desktop should hide the Playground tab. Same for any other directive whose semantics map to a UI permission.
Where the gap is (from a decompile of the bundled app)
The permission keys are defined and consumed correctly:
xB = {
AUTO_UPDATE: "auto-update",
CUSTOM_MCP_SERVERS: "non_registry_servers",
HELP_MENU: "help-menu",
PLAYGROUND_MENU: "playground-menu",
SETTINGS_REGISTRY_TAB: "settings-registry-tab",
};
// Used to gate the Playground tab:
t(xB.PLAYGROUND_MENU) && <PlaygroundMenuItem />
// canShow returns true by default if the key is missing:
function canShow(key) { return permissions[key] ?? true; }
But the React Query loader that polls the enterprise config only extracts state/warning/policy — it discards every directive:
function FH() {
let { data: e } = useQuery({
queryKey: ["enterprise", "config"],
queryFn: async () => { let {data} = await NH(); return data ?? null; },
refetchOnWindowFocus: true,
refetchInterval: ...,
});
return { state: e?.state, warning: e?.warning, policy: e?.policy };
// ← directives like e?.playground are not extracted, never reach the permissions context
}
Suggested fix
Extend the loader to translate directives into the permissions shape:
return {
state: e?.state,
warning: e?.warning,
policy: e?.policy,
permissions: {
"playground-menu": e?.playground?.value !== false,
"non_registry_servers": e?.non_registry_servers?.value !== false,
// … any other directive → permission mappings
},
};
…and feed the permissions field into the existing wB context that backs canShow. The rest of the UI is already wired.
Impact
- Enterprise administrators pushing UI-restriction directives (e.g. hide Playground for dev teams that should only run pre-approved MCP servers) see no behavior change in the Desktop, despite the directive being correctly signed, transported, and stored. The admin assumes the policy is broken.
- Similar bug for
non_registry_servers: thv serve enforces it at the API level (refuses to start non-registry servers), but the UI doesn't proactively hide the "add custom server" affordance — users hit the error only after attempting, and there's no admin-message banner.
- Related (separate issue): when the config server enters
offline_grace_period / offline_degraded, the Desktop DOES show a banner (good!) — but it does not display the degraded_mode.message value the admin configured in values.yaml. Worth folding in.
Versions tested: ToolHive Studio Enterprise 0.1.30 (built e8abecc).
Summary
The Enterprise Studio Desktop app polls
GET /enterprise/api/v1beta/config(provided by the enterprise overlay ofthv serve) and extracts onlystate,warning, andpolicyfrom the response — ignoring the individual policy directives in the payload (playground,non_registry_servers,registry,telemetry, …).As a result, when an administrator pushes a directive like
playground: { value: false, enforcement: enforced }via the enterprise config server, the directive is signed into envelopes, propagated correctly to the Desktop'sthv servesubprocess, and visible in the API response — but the Desktop UI takes no action. The Playground tab remains visible.The permission keys, the gate, and the React data flow are all wired — only the mapping from directive value to permission state is missing.
Repro
playgrounddirective:{ "playground": { "value": false, "enforcement": "enforced" }, "state": "online" }Expected
When
playground.value === falseis present in the envelope, the Desktop should hide the Playground tab. Same for any other directive whose semantics map to a UI permission.Where the gap is (from a decompile of the bundled app)
The permission keys are defined and consumed correctly:
But the React Query loader that polls the enterprise config only extracts
state/warning/policy— it discards every directive:Suggested fix
Extend the loader to translate directives into the permissions shape:
…and feed the
permissionsfield into the existingwBcontext that backscanShow. The rest of the UI is already wired.Impact
non_registry_servers:thv serveenforces it at the API level (refuses to start non-registry servers), but the UI doesn't proactively hide the "add custom server" affordance — users hit the error only after attempting, and there's no admin-message banner.offline_grace_period/offline_degraded, the Desktop DOES show a banner (good!) — but it does not display thedegraded_mode.messagevalue the admin configured invalues.yaml. Worth folding in.Versions tested: ToolHive Studio Enterprise 0.1.30 (built
e8abecc).