feat(#1065): web_tools tool group gates WebFetch/WebSearch built-ins

Replaces the earlier capability-based approach (closed #1069) with a
ToolGroup — capabilities are for privileged system access, web egress is
a tool permission.

Add ToolGroup::WebTools to hive-sh4re:
- tools() returns &[] (no MCP tools gated)
- builtin_tools() returns &["WebFetch", "WebSearch"] — new method on ToolGroup
- Present in ALL and as_str() → "web_tools"

In hive-ag3nt/mcp.rs:
- allowed_tools_arg() now iterates group.builtin_tools() to prepend any
  group-gated built-ins alongside the base ALLOWED_BUILTIN_TOOLS set
- builtin_tools_arg_for_flavor(flavor) replaces builtin_tools_arg() so
  the flavor-correct effective groups are used when building --tools
- builtin_tools_arg() kept as a flavor=Agent convenience alias
- turn.rs updated to call builtin_tools_arg_for_flavor(files.flavor) so
  manager sessions also see web tools when web_tools is in their groups

The dashboard T00L GR0UPS table gains a web_tools column automatically
(ToolGroup::ALL drives the columns).
This commit is contained in:
iris 2026-06-02 11:23:39 +02:00
commit f6b80cf02e
3 changed files with 58 additions and 11 deletions

View file

@ -1895,9 +1895,10 @@ impl ServerHandler for ManagerServer {}
/// tools as `mcp__<this>__<tool>` (e.g. `mcp__hyperhive__send`).
pub const SERVER_NAME: &str = "hyperhive";
/// Built-in claude tools the turn loop enables via `--tools`. Anything not
/// in this list literally doesn't exist in the session (claude won't even
/// try to call it). Web egress (`WebFetch`/`WebSearch`) and nested agents
/// Built-in claude tools always present in every session. Anything not
/// in this list (or added by `extra_builtin_tools`) literally doesn't
/// exist in the session. Web egress (`WebFetch`/`WebSearch`) are
/// tool-group-gated (`web_tools`) — off by default. Nested agents
/// (`Task`) are intentionally omitted. `Bash` is disallowed — shell
/// execution goes through `mcp__hyperhive__bash_run` (background tasks
/// with structured output) instead of a raw interactive shell. `TodoWrite`
@ -2035,13 +2036,19 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
/// both the built-ins and the MCP surface.
#[must_use]
pub fn allowed_tools_arg(flavor: Flavor) -> String {
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
.iter()
.map(|s| (*s).to_owned())
.collect();
let groups = effective_tool_groups(flavor);
// Base built-ins always present.
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS.iter().map(|s| (*s).to_owned()).collect();
// Extra built-ins gated by tool groups (e.g. WebFetch/WebSearch via web_tools).
for group in &groups {
for tool in group.builtin_tools() {
if !all.iter().any(|t| t == *tool) {
all.push((*tool).to_owned());
}
}
}
all.extend(allowed_mcp_tools(&groups));
// Capability-gated tools: added to --allowedTools when HIVE_CAPABILITIES
// Capability-gated MCP tools: added to --allowedTools when HIVE_CAPABILITIES
// includes the corresponding capability. hive-c0re performs a second
// server-side check, so this is a usability gate (no annoying prompts),
// not the security boundary.
@ -2052,10 +2059,28 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String {
}
/// Built-in tools list for `--tools` (which built-ins exist in this
/// session). Same as `ALLOWED_BUILTIN_TOOLS` but joined comma-separated.
/// session). Base set plus any group-gated built-ins (e.g.
/// `WebFetch`/`WebSearch` when the `web_tools` group is active).
#[must_use]
pub fn builtin_tools_arg() -> String {
ALLOWED_BUILTIN_TOOLS.join(",")
builtin_tools_arg_for_flavor(Flavor::Agent)
}
/// Flavor-aware variant used by `turn.rs` via `builtin_tools_arg`. Reads
/// the effective tool groups for `flavor` so `--tools` matches what
/// `--allowedTools` includes for the same session.
#[must_use]
pub fn builtin_tools_arg_for_flavor(flavor: Flavor) -> String {
let groups = effective_tool_groups(flavor);
let mut tools: Vec<&str> = ALLOWED_BUILTIN_TOOLS.to_vec();
for group in &groups {
for t in group.builtin_tools() {
if !tools.contains(t) {
tools.push(t);
}
}
}
tools.join(",")
}
/// Where the NixOS module writes the per-agent extra-MCP spec (see

View file

@ -672,7 +672,7 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool,
.arg(&files.mcp_config)
.arg("--strict-mcp-config")
.arg("--tools")
.arg(mcp::builtin_tools_arg())
.arg(mcp::builtin_tools_arg_for_flavor(files.flavor))
.arg("--allowedTools")
.arg(mcp::allowed_tools_arg(files.flavor));
let mut child = cmd

View file

@ -774,10 +774,18 @@ pub enum ToolGroup {
Diagnostics,
/// `bash_run`, `bash_status`
Execution,
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
/// `WebSearch` (search the web). Both are omitted from `--tools` by
/// default; adding this group to an agent enables them in the session
/// and in `--allowedTools` so they run without a confirmation prompt.
/// Does not gate any MCP tools — `tools()` returns `&[]`.
WebTools,
}
impl ToolGroup {
/// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group.
/// Returns `&[]` for `WebTools` — it enables Claude built-in tools,
/// not MCP tools; see `builtin_tools()`.
#[must_use]
pub fn tools(self) -> &'static [&'static str] {
match self {
@ -804,6 +812,18 @@ impl ToolGroup {
],
Self::Diagnostics => &["get_logs"],
Self::Execution => &["bash_run", "bash_status"],
Self::WebTools => &[],
}
}
/// The Claude built-in tool names enabled by this group. Only
/// `WebTools` returns a non-empty slice; all other groups return `&[]`
/// (they control MCP tools via `tools()` instead).
#[must_use]
pub fn builtin_tools(self) -> &'static [&'static str] {
match self {
Self::WebTools => &["WebFetch", "WebSearch"],
_ => &[],
}
}
@ -837,6 +857,7 @@ impl ToolGroup {
Self::Scheduling,
Self::Diagnostics,
Self::Execution,
Self::WebTools,
];
/// The `snake_case` wire name for this group (matches `serde(rename_all =
@ -852,6 +873,7 @@ impl ToolGroup {
Self::Scheduling => "scheduling",
Self::Diagnostics => "diagnostics",
Self::Execution => "execution",
Self::WebTools => "web_tools",
}
}
}