// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Review:Tools_impl; import :Tools; import std; namespace Crafter::Review::Tools { std::vector McpToolNames() { std::vector out; for (const std::string_view tool : McpTools) out.push_back(std::format("mcp__{}__{}", McpServer, tool)); return out; } std::vector McpDeniedToolNames() { std::vector out; for (const std::string_view tool : McpDenied) out.push_back(std::format("mcp__{}__{}", McpServer, tool)); return out; } const std::vector& Deny() { static const std::vector deny = [] { std::vector out = { "Edit", "Write", "MultiEdit", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent", "TodoWrite", "Bash(ssh:*)", "Bash(adb:*)", "Bash(rm:*)", "Bash(mv:*)", "Bash(cp:*)", "Bash(git push:*)", "Bash(git commit:*)", "Bash(git checkout:*)", "Bash(git reset:*)", "Bash(git am:*)", "Bash(make:*)", "Bash(npm:*)", "Bash(npx:*)", "Bash(node:*)", "Bash(supabase:*)", "Bash(docker:*)", "Bash(podman:*)", "Bash(psql:*)", }; std::ranges::move(McpDeniedToolNames(), std::back_inserter(out)); return out; }(); return deny; } const std::vector& BaseAllow() { static const std::vector allow = [] { std::vector out = { "Read", "Glob", "Grep", "Bash(cat:*)", "Bash(rg:*)", "Bash(grep:*)", "Bash(wc:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(sed -n:*)", "Bash(ls:*)", "Bash(find:*)", "Bash(diff:*)", "Bash(sha256sum:*)", "Bash(sort:*)", "Bash(uniq:*)", "Bash(cut:*)", "Bash(awk:*)", "Bash(echo:*)", "Bash(printf:*)", "Bash(cd:*)", "Bash(python3 -:*)", }; for (const std::string_view verb : GitReadOnly) out.push_back(std::format("Bash(git {}:*)", verb)); return out; }(); return allow; } namespace { // re.match(r'^Bash\((.+):\*\)$', tool).group(1): the prefix of a // Bash(:*) entry. `.` stops at a newline and `$` also matches // before a final one, so a prefix never holds a newline. std::optional BashPrefix(std::string_view tool) { std::string_view body = tool; if (body.ends_with('\n')) body.remove_suffix(1); if (body.size() < 9 || !body.starts_with("Bash(") || !body.ends_with(":*)")) return std::nullopt; const std::string_view prefix = body.substr(5, body.size() - 8); if (prefix.contains('\n')) return std::nullopt; return prefix; } } bool Denied(std::string_view tool) { if (std::ranges::contains(Deny(), tool)) return true; const std::optional prefix = BashPrefix(tool); if (!prefix) return false; return std::ranges::any_of(Deny(), [&](const std::string& entry) { const std::optional denied = BashPrefix(entry); return denied && (*prefix == *denied || prefix->starts_with(std::format("{} ", *denied))); }); } }