mcp: plugin_add takes a digest the MCP surface cannot discover, so an agent cannot enable an installed package #100
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#100
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The gap
plugin_addis the only plugin tool on the MCP surface. It requires exactly one ofdigest(a package already in the store) orfile(a path to a signed.cip). Nothing on that surface can produce either value.An agent session with the MCP tools and no shell therefore cannot enable a package that is sitting installed and approved-elsewhere in the machine-wide store, even though
plugin_addexists precisely to do that. The tool that acts has no companion tool that discovers.Measured, 2026-09-04, v0.26.1 (
4555887)On this machine the XAML reference package is installed in the user-scoped store and enabled for
E:\code\rust\code-index:The store holds the bytes machine-wide; the approval record is per project key, and only one record exists. Enabling it for a second project is a pure grant — no download, no re-install. The operator asked how to have an agent do that in each project, and the honest answer was "it cannot, from MCP alone."
What the MCP surface does report, and why it does not close this.
project_overview.plugin_activationcarriesactive_activation_digestandpending_activation_digest— the activation identity of the generation this project is serving, not a package install identity, and in a project that has enabled nothing there is no active generation to name. The block already carriespackages_requested_not_installed,package_duplicate_idsandpackage_set_consulted, so the store is demonstrably in reach at the point the block is built; the inventory simply is not in it.What the CLI has and MCP does not.
plugin statusis read-only (cmd_status: "READ ONLY, so no disclosure and no write") and itsinstalledlines come fromstore.installed()— the machine-wide store, independent of this project's approval record. That is the discovery answer, and it is reachable only through a shell.Why this is a product defect and not a workaround
The two available workarounds are both wrong in the way this repository's own guides call out:
CLAUDE.mdor a prompt. It works until the package version moves, at which point every copy of the constant becomes adigest_mismatchrefusal — and a stale pin looks like a stale download.plugin status. Correct today, but it makes the shell a hard dependency for a capability the MCP surface advertises, and it is exactly the shell fallbackCLAUDE.mdtells sessions not to reach for.Candidate repairs
Either would close it; the first is smaller.
plugin_addaccept a package id.de.h-dv.xamlresolved against the store, refusing with the candidate list when the id is ambiguous across versions — the same shape as bareplugin addrefusing when several.cipfiles sit under<root>/.code-index/plugins/. The operator still answers the confirmation; only the identifier becomes nameable.plugin_activation. One row per installed package — digest, id, version, languages, requested capabilities, and whether this project has approved it — which is theinstalled/requested APPROVED|NOT APPROVEDpairingplugin statusalready computes. This also answers "what could I enable here?", which the id-based repair does not.The second interacts with #71 (startup/tool-schema payload cap) and #73 (
project_overviewaggregates), so the inventory should be a bounded list with its own absent/empty distinction rather than an unbounded one — an EMPTY inventory is a measurement ("the store holds nothing"), and an ABSENT one means this daemon did not report, never zero.Acceptance
An agent with MCP tools and no shell, in a project that has approved nothing, can:
de.h-dv.xaml 0.1.0is installed in the store and not approved here;plugin_addwith an identifier it obtained from step 1;Step 1 is what is missing today.
Notes
_prdoc/guides/80-operator-recovery.md§0.1 is now stale and should be corrected in passing: it states the published package is unsigned and producessignature_missing("MEASURED 2026-09-02, by reading the release itself: v0.24.0 carries nine assets … There is node.h-dv.xaml-0.1.0.cips"). The release workflow has since gained its signing step, and v0.26.1 publishesde.h-dv.xaml-0.1.0.cips(112 bytes) pluscode-index-publisher.pub. §1.2's four-code repair table is still correct; the §0.1 preamble now describes a fixed defect as current, which sends an operator to generate their own signing key for no reason.Fixed — with the inventory repair, but split, because the rows measurably do not fit where this issue put them.
The repair chosen, and why
Repair 2 (the inventory), not repair 1 (the package id). This issue called repair 1 smaller, and it is — but it does not satisfy acceptance step 1. An agent given an id-accepting
plugin_addstill cannot learn what is installed; it can only act on a name it already knew. The inventory yields the digest, whichplugin_addalready accepts, so steps 2 and 3 need no change at all.The measurement that changed the design
This issue anticipated the interaction with #71 and #73 and asked for a bounded list. It turns out bounded is not enough — the rows do not fit on
project_overviewat any length. Measured on the daemon leg by suppressing the block and re-running:project_overview(ceiling 4300)agent_task_plugin_benchplugin-wpf (ceiling 7425.6)225 wire tokens per call on a project with one installed package, against 25 and 93 tokens of headroom. Two independent gates said no, with numbers.
So it is split:
project_overviewtool carries{availability, installed_total}plus aninventoryURI when there is something to point at — 12 wire tokens of delta;code-index://project/overviewandcode-index://stats— unratcheted, pulled once, and now graded by #107's resource grader, which landed alongside this.The store path was dropped from the tool block entirely: it is machine-dependent, and a ratcheted constant payload must not carry a machine-dependent value.
plugin_add'sinvalid_argumentshint points at the block, which costs nothing on the success path.Reuses
cmd_status's pairing through the same two primitives —Store::installed()andApprovalRecord::grant_for— rather than a second definition, as this issue asked.PACKAGE_STORE_INVENTORY_CAP = 20, registered inbounding_site_registry.rsasinstalled_total + installed_truncated.Four states, never collapsed
unavailable, no countsUnavailable(why)unavailable+refusalinstalled_total: 0installedabsent, not[]The third is the one this issue called out: an EMPTY inventory means the store holds nothing; an ABSENT one means this daemon did not report. They render differently.
Mutations, all RUN
take(0)on the rows → RED,code-index://project/overview must carry 'installed'a non-zero count must name where the rows are: {"availability":"reported","installed_total":1}approved_here: false→ RED,the pairing must MOVEPlus an in-file unit test for all four renderings.
_prdoc/guides/80-operator-recovery.md§0.1 correctedVerified against the real release before editing, per this issue's note. v0.26.1 publishes 13 assets including
de.h-dv.xaml-0.1.0.cips(112 bytes) andcode-index-publisher.pub, andrelease.ymlboth signs and grades thesignature_invalid/signature_missingrefusals against the shipped binary. The stale paragraph is replaced, not deleted, and says why — so the next reader can tell a corrected claim from one that was never made.Two method findings from the lane, both worth keeping
assert_ne!between two whole blocks is weaker than it looks. The collapse mutation survived the inequality pair, because the collapsed rendering still differed by an incidentalstorefield. What caught it was the arm naming the fields an unavailable exit may not carry. Both are kept, and the note records which one actually fired.One constraint this leaves behind
project_overviewis at 4293 of 4300 on the daemon leg. The next disclosure added there will not fit. That is a hard fact for whoever picks up #111 (the payload category split) or #73 — noted there.overview_payload_budget_e2eis safe from the unconsulted-package-set race by luck, not by design — 25 tokens of headroom against a ~190-token block #133Closing as already fixed — this describes a gap that
1aa6514(2026-09-04) had already closed before the issue was written.Verified in this tree, not taken on report:
PackageStoreInventoryis a struct atcrates/mcp-server/src/server.rs:3555with 4 resolved references.git merge-base --is-ancestor 1aa6514 HEAD→ yes, so it is in the master line this issue was filed against.plugin_addwants is nameable from the MCP surface.server.rs:3681spells the follow-through out for the caller: "and no re-install: callplugin_addwith thatdigestand answer the confirmation."Cause of the bad report, which is the part worth keeping. The measurements in the issue are correct about the binary that produced them and stale about the tree they were checked against. The session read HEAD (
7b3fc7c) but dogfoodedcode-index 0.26.1 (4555887)installed atc:\tools, and1aa6514landed after v0.26.1 was cut. Every "the MCP surface cannot produce this" statement was true of the running binary and false of the source beside it.This is the same skew that has bitten here before, and the guard against it is cheap: run
code-index doctor(or compare--versionagainstgit log) before explaining field behaviour from HEAD, and reinstall from master before a dogfooding pass. Not doing that manufactured a duplicate issue against shipped work.What does not survive the closure, so nothing is silently dropped:
_prdoc/guides/80-operator-recovery.md§0.1 note in the issue's Notes section is likewise already handled — lines 85-93 now carry an explicit "An earlier edition of this section said the published package was unsigned … That was true of v0.24.0 and is not true now", kept rather than deleted precisely because an operator who followed it generated a signing key they did not need.plugin_addwith an identifier it obtained itself) is worth keeping as a gate if one does not already exist against the shipped payload. If it is not covered, that belongs in a fresh issue scoped to the gate rather than reopening this one.