Context
feat/plugin-discovery (PR #2) adds src/search.ts (the plugin-discovery API: searchPlugins / createPluginSearch / createTtlCache / sourceToInstallInput). It is green (tsc, 100% coverage, eslint, quire) and was smoke-tested live (found 8 @agent-ix/spec-* packages on npm). A /code-review + /gap-analysis pass (SpecReview SR-005, reviews/26-06-27-plugin-discovery-gap-analysis.md) found the items below. None are merge-blockers, but the first two are real (privacy + memory) and one is a spec↔code divergence.
Tasks
Acceptance criteria
- Distinct non-empty tokens → distinct cache entries (test); no token value ever appears in a key (CON-2 still holds).
- Cache bounded by default + LRU-eviction test.
- Manifest name with
.. is rejected/encoded (test).
src/search.ts stays at 100% coverage; quire validate --scope . "spec/**/*.md" clean.
- Gap-analysis verdict clears toward PASS.
References
Context
feat/plugin-discovery(PR #2) addssrc/search.ts(the plugin-discovery API:searchPlugins/createPluginSearch/createTtlCache/sourceToInstallInput). It is green (tsc, 100% coverage, eslint, quire) and was smoke-tested live (found 8@agent-ix/spec-*packages on npm). A/code-review+/gap-analysispass (SpecReviewSR-005,reviews/26-06-27-plugin-discovery-gap-analysis.md) found the items below. None are merge-blockers, but the first two are real (privacy + memory) and one is a spec↔code divergence.Tasks
createPluginSearchcacheKey(src/search.ts~line 568) keys on token presence (token ? "auth" : "anon"). FR-010 §Behavior (spec/functional/FR-010-discovery-cache.md~line 44) specifies "a stable non-secret discriminator of the resolved token". Two distinct tokens currently collide on"auth", so with the late-bound-token feature (FR-010-AC-5 / TC-036) a shared searcher could serve user A's authenticated (incl. private-repo) GitHub results to user B. Fix: fold a stable non-secret hash of the resolved token (e.g. first 8 hex of SHA-256, computed vianode:crypto) into the key instead of the boolean. Add a test: two different non-empty tokens produce distinct cache entries (no cross-token hit).createPluginSearchleavescacheMaxundefinedby default (src/search.ts~line 546), socreateTtlCachesets nomaxand entries evict only lazily on read of an expired key — memory grows with distincttag|query|sources|limitsearches over the TTL. Fix: defaultcacheMax(e.g. 256) increatePluginSearch. Add a test: LRU eviction past the bound.manifestUrl(src/search.ts~line 421) interpolates the registry-suppliedname/fullNameunencoded intohttps://unpkg.com/{name}/{manifestPath}andhttps://raw.githubusercontent.com/{fullName}/HEAD/{manifestPath}. Cross-host SSRF is not reachable (fixed authority), but a name containing..can traverse within the trusted CDN. Fix: reject/encode path segments containing..or control chars in the registry-derived name before building the URL. (manifestPathis host-supplied → trusted.)parseRateNumber(header)(src/search.ts~line 331) has no non-finite guard → surfacesNaNrate fields; treat a non-finite parse as "no rate info" (returnundefined). A cache hit returns the sharedSearchResponsereference (~line 576) → return a shallow clone or document immutability.plan/Plan-001-plugin-discovery/tasks/Task-001..007status: todo → done(they are implemented + verified).Task-008(publish) stays pending.Acceptance criteria
..is rejected/encoded (test).src/search.tsstays at 100% coverage;quire validate --scope . "spec/**/*.md"clean.References
feat/plugin-discoverySR-005(reviews/26-06-27-plugin-discovery-gap-analysis.md)spec/functional/FR-010-discovery-cache.md,FR-009-compatibility-verification.md,FR-008-candidate-search.md