From d8e29fcf5177c10e9c5e308d18e2d77bd01a9ff7 Mon Sep 17 00:00:00 2001 From: Shiju Date: Fri, 6 Nov 2026 23:36:26 +0530 Subject: [PATCH 1/2] fix(tui): show MCP tool selectors in policy viewer Signed-off-by: Shiju --- crates/openshell-tui/src/lib.rs | 223 +++++++++++++++++++++++ docs/how-it-works/sandboxes/overview.mdx | 2 + 2 files changed, 225 insertions(+) diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 133d7d5c0a..0e807cd452 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1315,6 +1315,16 @@ fn render_policy_lines( // L7 allow rules. for l7 in &ep.rules { if let Some(allow) = &l7.allow { + if ep.protocol.eq_ignore_ascii_case("mcp") { + render_mcp_rule( + &mut lines, + "Allow", + &allow.method, + allow.params.get("name"), + t, + ); + continue; + } let method = if allow.method.is_empty() { "*" } else { @@ -1334,6 +1344,18 @@ fn render_policy_lines( } } + if ep.protocol.eq_ignore_ascii_case("mcp") { + for deny in &ep.deny_rules { + render_mcp_rule( + &mut lines, + "Deny", + &deny.method, + deny.params.get("name"), + t, + ); + } + } + // Access preset (if set instead of explicit rules). if ep.access != 0 && ep.rules.is_empty() { lines.push(Line::from(vec![ @@ -1371,6 +1393,37 @@ fn render_policy_lines( lines } +fn render_mcp_rule( + lines: &mut Vec>, + action: &str, + method: &str, + tool: Option<&openshell_core::proto::L7QueryMatcher>, + theme: &theme::Theme, +) { + use ratatui::text::{Line, Span}; + + let method = if method.is_empty() { + if tool.is_some() { "tools/call" } else { "*" } + } else { + method + }; + lines.push(Line::from(vec![ + Span::styled(format!(" {action}: "), theme.muted), + Span::styled(method.to_string(), theme.text), + ])); + if let Some(tool) = tool { + for name in std::iter::once(&tool.glob) + .filter(|name| !name.is_empty()) + .chain(&tool.any) + { + lines.push(Line::from(vec![ + Span::styled(" Tool: ", theme.muted), + Span::styled(name.clone(), theme.text), + ])); + } + } +} + // --------------------------------------------------------------------------- // Animation helper // --------------------------------------------------------------------------- @@ -3444,3 +3497,173 @@ mod sandbox_notes_tests { assert_eq!(sandbox_notes(&sandbox, String::new()), ""); } } + +#[cfg(test)] +mod policy_render_tests { + use super::*; + + fn policy_lines(yaml: &str) -> Vec> { + let policy = openshell_policy::parse_sandbox_policy(yaml).expect("valid policy"); + render_policy_lines(&policy, &theme::Theme::dark()) + } + + #[test] + fn mcp_tool_allowlist_is_visible_one_tool_per_line() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: host.openshell.internal + port: 32443 + protocol: mcp + enforcement: enforce + rules: + - allow: + method: tools/call + tool: + any: + - local_gh_issue_view + - local_gh_pr_checks + - local_gh_pr_details + - local_gh_pr_diff + - local_gh_pr_list + - local_gh_pr_view + - local_gh_repo_view + - local_gh_run_failed_logs + - local_gh_run_view + - local_gh_source_blob + - local_gh_source_tree + - local_gh_version +", + ); + let tools: Vec<_> = lines + .iter() + .map(ToString::to_string) + .filter_map(|line| line.strip_prefix(" Tool: ").map(str::to_string)) + .collect(); + assert_eq!( + tools, + [ + "local_gh_issue_view", + "local_gh_pr_checks", + "local_gh_pr_details", + "local_gh_pr_diff", + "local_gh_pr_list", + "local_gh_pr_view", + "local_gh_repo_view", + "local_gh_run_failed_logs", + "local_gh_run_view", + "local_gh_source_blob", + "local_gh_source_tree", + "local_gh_version", + ] + ); + assert!( + lines + .iter() + .any(|line| line.to_string() == " Allow: tools/call") + ); + assert!( + !lines + .iter() + .any(|line| line.to_string().contains("tools/call *")) + ); + + let mut buffer = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(0, 0, 60, 30)); + ratatui::widgets::Widget::render( + ratatui::widgets::Paragraph::new(lines), + buffer.area, + &mut buffer, + ); + for tool in tools { + assert!(buffer.content.chunks(60).any(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .contains(&tool) + })); + } + } + + #[test] + fn mcp_methods_and_allow_deny_selectors_preserve_their_scope() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: mcp.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: + method: tools/list + - allow: + tool: read_source + - allow: + method: tools/call + params: + name: + any: [read_*, list_*] + deny_rules: + - tool: read_private + - method: tools/call + params: + name: + any: [read_secret_*, list_private] + - method: resources/read + profile: + endpoints: + - host: profile.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: {} + deny_rules: + - {} + rest: + endpoints: + - host: api.example.com + port: 443 + protocol: rest + rules: + - allow: {method: GET, path: /repos/**} +", + ); + let text: Vec<_> = lines.iter().map(ToString::to_string).collect(); + assert!(text.iter().any(|line| line == " Allow: *")); + assert!(text.iter().any(|line| line == " Deny: *")); + let mcp_start = text + .iter() + .position(|line| line == " Allow: tools/list") + .unwrap(); + assert_eq!( + &text[mcp_start..mcp_start + 12], + [ + " Allow: tools/list", + " Allow: tools/call", + " Tool: read_source", + " Allow: tools/call", + " Tool: read_*", + " Tool: list_*", + " Deny: tools/call", + " Tool: read_private", + " Deny: tools/call", + " Tool: read_secret_*", + " Tool: list_private", + " Deny: resources/read", + ] + ); + assert!( + text.iter() + .any(|line| line == " Allow: GET /repos/**") + ); + } +} diff --git a/docs/how-it-works/sandboxes/overview.mdx b/docs/how-it-works/sandboxes/overview.mdx index 2ac6a4f69d..14659119c7 100644 --- a/docs/how-it-works/sandboxes/overview.mdx +++ b/docs/how-it-works/sandboxes/overview.mdx @@ -822,6 +822,8 @@ The dashboard has three panels stacked vertically: Gateways, Providers (or Globa The sandbox table’s NOTES column shows `Invalid config` when policy or provider configuration blocks provisioning. Open the sandbox detail view for the full rejection reason, or run `openshell sandbox get -o json`. The note clears after the configuration is repaired; active port forwards remain listed. +In the sandbox detail view, the Policy tab lists MCP allow and deny methods with each selected tool or tool-name pattern on its own line. Scroll through the pane to inspect the complete list. These entries show the configured policy rules; they do not establish whether an MCP tool is read-only. + Press `c` in the Sandboxes panel to create a sandbox. In the optional Command field, use quotes or backslashes to group arguments containing spaces. For example, `/bin/sh -c "echo GOOD; read x"` prints `GOOD` and waits for Enter. Invalid quoting keeps the form open without creating a sandbox. The field does not expand variables or evaluate shell operators; invoke a shell explicitly with `sh -c` when you need that behavior. ## Port Forwarding From 61a6dc3fb4fb0fada9d8bad0c0467501292119fa Mon Sep 17 00:00:00 2001 From: Shiju Date: Sat, 7 Nov 2026 01:43:36 +0530 Subject: [PATCH 2/2] chore(tui): keep policy display fix scoped to code Signed-off-by: Shiju --- docs/how-it-works/sandboxes/overview.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/how-it-works/sandboxes/overview.mdx b/docs/how-it-works/sandboxes/overview.mdx index 14659119c7..2ac6a4f69d 100644 --- a/docs/how-it-works/sandboxes/overview.mdx +++ b/docs/how-it-works/sandboxes/overview.mdx @@ -822,8 +822,6 @@ The dashboard has three panels stacked vertically: Gateways, Providers (or Globa The sandbox table’s NOTES column shows `Invalid config` when policy or provider configuration blocks provisioning. Open the sandbox detail view for the full rejection reason, or run `openshell sandbox get -o json`. The note clears after the configuration is repaired; active port forwards remain listed. -In the sandbox detail view, the Policy tab lists MCP allow and deny methods with each selected tool or tool-name pattern on its own line. Scroll through the pane to inspect the complete list. These entries show the configured policy rules; they do not establish whether an MCP tool is read-only. - Press `c` in the Sandboxes panel to create a sandbox. In the optional Command field, use quotes or backslashes to group arguments containing spaces. For example, `/bin/sh -c "echo GOOD; read x"` prints `GOOD` and waits for Enter. Invalid quoting keeps the form open without creating a sandbox. The field does not expand variables or evaluate shell operators; invoke a shell explicitly with `sh -c` when you need that behavior. ## Port Forwarding