The MCP plugin_add tool cannot grant derived_names, so an agent can never add a package that needs it #106

Closed
opened 2026-09-04 15:01:35 +02:00 by buildagent · 1 comment
Member

Split out of #103, which shipped the grant path through the CLI. The MCP tool was left untouched only because crates/mcp-server/src/server.rs was held by a concurrent lane.

Current behaviour: fail-closed and correct, but a dead end

grant: ["requested"] does resolve derived_names: true through plan_grant — that part works. But plugin_add then drops the bool and calls conform::check(..., false). So C1 runs without the authority the package needs, the verdict fails, and the add is refused.

That is at least not silent: the refusal names fact.span_name_mismatch and the withheld-grant witness explains it. But there is no answer an agent can give that gets past it, which is the same defect class #103 fixed for the CLI, and the same class as plugin_add's old poll instruction naming a state that could never become true.

The fix

Three lines: thread grant.derived_names into conform::check_with_grant and into .with_derived_names(..) on the approval. check_with_grant already exists and already records the answer on the verdict, so require_verdict_covering_grant will accept the result.

The test

An MCP-leg e2e adding the ruby package with grant: ["requested"] and asserting the file indexes — mutated by reverting the thread-through, which must go red at the add, not merely at a later query.

Also worth checking in the same pass: the tool's note must not name a state that this path cannot reach. crates/mcp-server/tests/tool_notes_name_reachable_states.rs already grades that property and should cover whatever the note says about derived_names.

Follow-up to #103. Same family as #104 (re-grant does not reindex) and #105 (aggregate cost of declining).

Split out of #103, which shipped the grant path through the CLI. The MCP tool was left untouched only because `crates/mcp-server/src/server.rs` was held by a concurrent lane. ## Current behaviour: fail-closed and correct, but a dead end `grant: ["requested"]` does resolve `derived_names: true` through `plan_grant` — that part works. But `plugin_add` then **drops the bool** and calls `conform::check(..., false)`. So C1 runs without the authority the package needs, the verdict fails, and the add is refused. That is at least not silent: the refusal names `fact.span_name_mismatch` and the withheld-grant witness explains it. But there is no answer an agent can give that gets past it, which is the same defect class #103 fixed for the CLI, and the same class as `plugin_add`'s old poll instruction naming a state that could never become true. ## The fix Three lines: thread `grant.derived_names` into `conform::check_with_grant` and into `.with_derived_names(..)` on the approval. `check_with_grant` already exists and already records the answer on the verdict, so `require_verdict_covering_grant` will accept the result. ## The test An MCP-leg e2e adding the ruby package with `grant: ["requested"]` and asserting the file indexes — mutated by reverting the thread-through, which must go red at the add, not merely at a later query. Also worth checking in the same pass: the tool's `note` must not name a state that this path cannot reach. `crates/mcp-server/tests/tool_notes_name_reachable_states.rs` already grades that property and should cover whatever the note says about `derived_names`. ## Related Follow-up to #103. Same family as #104 (re-grant does not reindex) and #105 (aggregate cost of declining).
Author
Member

Fixed. And the second mutation found a quieter defect this issue did not name.

plan_grant's answer is now threaded into conform::check_with_grant, Approval::with_derived_names, and require_verdict_covering_grant (replacing require_passing_verdict, matching cmd_add).

The payload gains derived_names + derived_names_semantics on both the approved and the installed_inert replies. That second placement matters: Grant::render returns capabilities and bridges only, so an empty granted list says nothing about this authority — and silence there would read as "not granted" when it might be granted, which is the absent-reads-as-negative shape the project refuses.

The test, and its anti-vacuity half

the_mcp_add_can_grant_derived_names in plugin_add_mcp_e2e.rs, driving a new fixture package de.h-dv.mcpderived whose extractor returns a real code_index_abi::Encoder frame carrying a derived-name ref — Gamma at the span of alpha, same length and in range, so the only check it can fail is name-equals-bytes. That construction is what makes the test about this authority rather than about validation in general.

Paired with the same package under grant: ["none"], which must be refused. Without that half, C1's dependence on the authority would be assumed rather than measured.

Mutations, both RUN

1 — revert check_with_grant → check. RED at the add:

the same package, the same answer, the requested grant:
{"error":"conformance_failed","hint":"the bytes are installed and the grant was NOT written
 — a package failing C1 cannot be enabled"}
  left: Null   right: "approved"

2 — drop .with_derived_names(..). RED, and it exposed a defect this issue does not name and I would not have predicted: the add succeeds and the record silently drops the authority.

Approval { digest: "sha256:98f6b2…", package_id: "de.h-dv.mcpderived",
           capabilities: [], bridges: [], auto_enabled_from: None,
           derived_names: false }

An operator answering yes, an add reporting success, and a stored record saying no. That is worse than the refusal this issue was filed about, because a refusal is visible.

One thing NOT independently graded, stated rather than claimed

Swapping require_verdict_covering_grant back to require_passing_verdict stays green. At this call site the verdict's authority and the grant's are the same bool by construction, so the stronger check has nothing extra to catch here. It is kept for front-end parity with cmd_add — and disclosed as untested rather than counted as covered.

tool_notes_name_reachable_states.rs still passes; the note names no derived_names state, so there is nothing there to become unreachable.

## Fixed. And the second mutation found a quieter defect this issue did not name. `plan_grant`'s answer is now threaded into `conform::check_with_grant`, `Approval::with_derived_names`, and `require_verdict_covering_grant` (replacing `require_passing_verdict`, matching `cmd_add`). The payload gains `derived_names` + `derived_names_semantics` on **both** the approved and the `installed_inert` replies. That second placement matters: `Grant::render` returns capabilities and bridges only, so an empty `granted` list says nothing about this authority — and silence there would read as "not granted" when it might be granted, which is the absent-reads-as-negative shape the project refuses. ### The test, and its anti-vacuity half `the_mcp_add_can_grant_derived_names` in `plugin_add_mcp_e2e.rs`, driving a new fixture package `de.h-dv.mcpderived` whose extractor returns a **real `code_index_abi::Encoder` frame** carrying a derived-name ref — `Gamma` at the span of `alpha`, same length and in range, so **the only check it can fail is name-equals-bytes**. That construction is what makes the test about this authority rather than about validation in general. Paired with the same package under `grant: ["none"]`, which must be **refused**. Without that half, C1's dependence on the authority would be assumed rather than measured. ### Mutations, both RUN **1 — revert `check_with_grant` → `check`.** RED at the add: ``` the same package, the same answer, the requested grant: {"error":"conformance_failed","hint":"the bytes are installed and the grant was NOT written — a package failing C1 cannot be enabled"} left: Null right: "approved" ``` **2 — drop `.with_derived_names(..)`.** RED, and it exposed a defect this issue does not name and I would not have predicted: the add **succeeds** and the record silently drops the authority. ``` Approval { digest: "sha256:98f6b2…", package_id: "de.h-dv.mcpderived", capabilities: [], bridges: [], auto_enabled_from: None, derived_names: false } ``` An operator answering yes, an add reporting success, and a stored record saying no. That is worse than the refusal this issue was filed about, because a refusal is visible. ### One thing NOT independently graded, stated rather than claimed Swapping `require_verdict_covering_grant` back to `require_passing_verdict` stays **green**. At this call site the verdict's authority and the grant's are the same bool by construction, so the stronger check has nothing extra to catch here. It is kept for front-end parity with `cmd_add` — and disclosed as untested rather than counted as covered. `tool_notes_name_reachable_states.rs` still passes; the note names no `derived_names` state, so there is nothing there to become unreachable.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#106
No description provided.