Skip to content

feat(runtime): granular per-tool/per-path/per-rule MCP taint policy + dashboard tree editor #3050

Description

@DaBlitzStein

Motivation

LibreFang's MCP taint scanner protects against credential/PII exfiltration through tool calls. Today the only server-level knob is:

[mcp_servers.my_server]
taint_scanning = false   # all-or-nothing

This forces operators into an uncomfortable trade-off: if any tool on a given MCP server produces false positives (e.g. a browser-automation server whose navigate tool returns opaque tab handles that look like tokens), the only recourse is to disable scanning for the entire server — losing protection for every other tool on that server.

The existing taint_policy field already supports per-tool, per-path rule exemptions (skipping individual TaintRuleIds like opaque_token or sensitive_key_name for specific JSONPath expressions). That mechanism is powerful but has several gaps:

  1. No tool-level default action — there is no way to say "skip scanning entirely for tool X, but keep scanning everything else on this server" without enumerating every argument path of tool X.
  2. No path-pattern wildcards — exemption keys are exact JSONPath strings; glob-style patterns like $.items[*] partially work via prefix matching but are not documented or tested as a first-class feature.
  3. No named, reusable rule sets — each server's taint_policy is self-contained. There is no way to define a shared named rule (with a custom severity action) and reference it from multiple servers or tools.
  4. No dashboard support — the taint policy is config-only; the dashboard has no UI for viewing or editing per-tool/per-path exemptions.

Proposed Design

1. Tool-level default action

Add a default field to McpTaintToolPolicy that controls what happens to arguments not matched by any path rule:

[mcp_servers.camofox.taint_policy.tools.navigate]
default = "skip"           # skip all taint scanning for this tool
# OR
default = "scan"           # (current implicit default) scan everything

This lets operators exempt a noisy tool with a single line rather than pattern-matching every argument path.

Rust shape (additive, non-breaking):

pub struct McpTaintToolPolicy {
    /// "scan" (default) or "skip" — applies to paths not covered by `paths`.
    #[serde(default)]
    pub default: McpTaintToolAction,
    pub paths: HashMap<String, McpTaintPathPolicy>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum McpTaintToolAction { #[default] Scan, Skip }

2. Path-pattern wildcards

Document and test glob-style wildcards in path exemption keys:

[mcp_servers.camofox.taint_policy.tools.read_file.paths]
"$.content"   = { skip_rules = ["opaque_token"] }
"$.metadata.*" = { skip_rules = ["sensitive_key_name"] }

$.metadata.* should exempt any immediate child key of metadata (e.g. $.metadata.etag, $.metadata.x-request-id). This already partially works via prefix matching in resolve_skip_rules but is undocumented and has no glob semantics for [*] within arrays.

3. Named, reusable taint rule sets

Add a top-level [[taint_rules]] table in config.toml for defining named rule groups with a severity action:

[[taint_rules]]
name = "browser_handles"
action = "skip"               # skip, warn, or block (default: block)
rules = ["opaque_token"]      # TaintRuleId variants to match

[[taint_rules]]
name = "pii_relaxed"
action = "warn"               # log but don't block
rules = ["pii_email", "pii_phone"]

Reference from per-tool policy:

[mcp_servers.camofox.taint_policy.tools.navigate]
rule_sets = ["browser_handles"]   # apply named sets on top of path rules

The action field extends the current binary block/allow into three tiers:

  • block — current behaviour: abort the tool call and return an error to the LLM.
  • warn — emit a structured log event at WARN level but allow the call through.
  • log / audit — emit at INFO level, allow through, useful for building baselines before enabling enforcement.

4. Dashboard tree editor

Add a taint policy panel to the MCP server configuration page in the React dashboard. The panel should render a three-level tree:

▼ camofox (MCP server)
  ▼ navigate (tool)          [default: scan ▾]
    $.tabId                  skip: opaque_token  [edit] [×]
    $.sessionId              skip: opaque_token  [edit] [×]
    [+ add path exemption]
  ▼ read_file (tool)         [default: scan ▾]
    [+ add path exemption]
  [+ add tool policy]

Requirements for the UI:

  • Query the existing /api/agents / config endpoints (or a new /api/mcp/servers endpoint if needed) to populate server and tool lists.
  • Mutations must call invalidateQueries with factory keys per the dashboard data-layer rule.
  • All state goes through hooks in src/lib/queries/ and src/lib/mutations/; no inline fetch() in page components.
  • Rule IDs (TaintRuleId variants) are shown as human-readable labels with a tooltip explaining what each rule catches.

What Already Exists

For context, the following is already in tree and working:

  • McpTaintPolicy { tools: HashMap<String, McpTaintToolPolicy> } — per-tool map.
  • McpTaintToolPolicy { paths: HashMap<String, McpTaintPathPolicy> } — per-path map within a tool.
  • McpTaintPathPolicy { skip_rules: Vec<TaintRuleId> } — rule-level exemption at a specific path.
  • TaintRuleId enum with nine variants: AuthorizationLiteral, KeyValueSecret, WellKnownPrefix, OpaqueToken, PiiEmail, PiiPhone, PiiCreditCard, PiiSsn, SensitiveKeyName.
  • resolve_skip_rules() in librefang-runtime-mcp applies path policies during scanning.

The proposals above are additive on top of this foundation and are backward-compatible (all new fields have #[serde(default)]).

Acceptance Criteria

  • McpTaintToolPolicy gains a default: McpTaintToolAction field (scan | skip); skip bypasses scanning for the tool without requiring path entries.
  • Path-pattern wildcards (.*, [*]) are documented in McpTaintPathPolicy rustdoc and covered by unit tests.
  • Top-level [[taint_rules]] config table added with name, action (block | warn | log), and rules fields; McpTaintToolPolicy gains a rule_sets field referencing them by name.
  • Dashboard taint policy panel renders the server → tool → path tree and supports adding/editing/removing entries without a full config reload.
  • All new config fields are covered by round-trip serde tests.
  • cargo clippy --workspace --all-targets -- -D warnings passes with zero new warnings.

Open Questions

  • Should warn/log severity actions write to a dedicated audit log table (SQLite via librefang-memory) so operators can replay what would have been blocked? Or is structured tracing output sufficient for an MVP?
  • Should the dashboard tree editor support drag-and-drop reordering of rule sets, or is alphabetical display sufficient?
  • Is there appetite for a dry_run = true server-level flag that applies warn to all rules globally, to help operators build exemption lists before going to block?

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiREST/WS endpoints and dashboardarea/runtimeAgent loop, LLM drivers, WASM sandboxarea/securitySecurity systems and auditingenhancementNew feature or requesthas-prA pull request has been linked to this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions