feat(mcp): add resource group scopes with permission inheritance

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-07 02:18:28 +08:00
1 parent 2eaf7f9914
commit 691ecd084b
31 files changed
+2398 -1047

No files matched your search

+13 -14
View File
@@ -81,7 +81,7 @@ describe("MCP policy settings state", () => {
it("guards the mutation entry point and wires the shared disabled state to policy controls", () => {
expect(settingsDialogSource).toContain("if (mcpPolicyControlsDisabled.value) return;");
expect(settingsDialogSource).toContain(':disabled="mcpPolicyControlsDisabled"');
expect(settingsDialogSource).toContain('@update:allowed-connection-ids="onMcpAllowedConnectionIdsChange"');
expect(settingsDialogSource).toContain('@update:scope="onMcpResourceScopeChange"');
const loadingStart = settingsDialogSource.indexOf("mcpPolicyLoading.value = true;");
const policyLoad = settingsDialogSource.indexOf("await settingsStore.initMcpGlobalPolicy(true);");
@@ -91,23 +91,22 @@ describe("MCP policy settings state", () => {
expect(loadingEnd).toBeGreaterThan(policyLoad);
});
it("keeps translated mode descriptions in one responsive layout track", () => {
const descriptionStart = settingsDialogSource.indexOf("data-mcp-execution-mode-description");
const descriptionEnd = settingsDialogSource.indexOf("settings.mcpCapabilityTitle", descriptionStart);
const descriptionSource = settingsDialogSource.slice(descriptionStart, descriptionEnd);
expect(descriptionStart).toBeGreaterThan(-1);
expect(descriptionEnd).toBeGreaterThan(descriptionStart);
expect(descriptionSource).toContain('class="grid text-xs"');
expect(descriptionSource.match(/col-start-1 row-start-1/g)).toHaveLength(3);
expect(descriptionSource.match(/\? 'visible' : 'invisible'/g)).toHaveLength(3);
it("renders one static mode hint in place of stacked per-mode description tracks", () => {
expect(settingsDialogSource).not.toContain("data-mcp-execution-mode-description");
const groupStart = settingsDialogSource.indexOf('role="radiogroup" aria-labelledby="mcp-execution-mode-label"');
const hintIndex = settingsDialogSource.indexOf("settings.mcpPermissionGlobalDefaultHint");
expect(groupStart).toBeGreaterThan(-1);
expect(hintIndex).toBeGreaterThan(groupStart);
});
it("keeps execution mode cards accessible as a keyboard radio group", () => {
expect(settingsDialogSource).toContain('role="radiogroup" aria-labelledby="mcp-execution-mode-label"');
expect(settingsDialogSource.match(/role="radio"/g)).toHaveLength(3);
expect(settingsDialogSource).toContain(":aria-checked=\"mcpExecutionMode === 'safe_write'\"");
expect(settingsDialogSource).toContain("onMcpExecutionModeKeydown($event, 'safe_write')");
expect(settingsDialogSource).toContain('v-for="mode in mcpExecutionModeOptions"');
expect(settingsDialogSource.match(/role="radio"/g)).toHaveLength(1);
expect(settingsDialogSource).toContain(':aria-checked="mcpExecutionMode === mode"');
expect(settingsDialogSource).toContain(':tabindex="mcpExecutionMode === mode ? 0 : -1"');
expect(settingsDialogSource).toContain("onMcpExecutionModeKeydown($event, mode)");
expect(settingsDialogSource).toContain('@keydown="onMcpExecutionModeKeydown($event, mode)"');
});
it("keeps MCP client config tabs on a single scrollable row", () => {
+8
View File
@@ -250,12 +250,20 @@ export interface McpGlobalPolicy {
readOnly: boolean;
allowDangerousSql: boolean;
allowedConnectionIds: string[] | null;
allowedGroupIds: string[];
allowedToolNames: string[] | null;
connectionPolicies: McpConnectionPolicy[];
groupPolicies: McpGroupPolicy[];
configured: boolean;
queryTimeoutSecs: number | null;
}
export interface McpGroupPolicy {
groupId: string;
readOnly: boolean;
allowDangerousSql: boolean;
}
export interface McpConnectionPolicy {
connectionId: string;
readOnly: boolean;
@@ -337,6 +337,24 @@ export function buildConnectionGroupPathMap(layout: SidebarLayout): Map<string,
return paths;
}
/** Build root-to-leaf stable group-id paths for permission inheritance. */
export function buildConnectionGroupIdPathMap(layout: SidebarLayout): Map<string, string[]> {
const paths = new Map<string, string[]>();
const visit = (entries: SidebarOrderEntry[], path: string[]) => {
for (const entry of entries) {
if (entry.type === "connection") {
paths.set(entry.id, path);
continue;
}
visit(entryChildren(entry), [...path, entry.id]);
}
};
visit(layout.order, []);
return paths;
}
export interface ConnectionGroupDestinationRow {
id: string;
name: string;