Skip to content

Wire the tool optimizer onto the Serve path#5543

Merged
tgrunnagle merged 4 commits into
mainfrom
vmcp-core_issue_5538
Jun 17, 2026
Merged

Wire the tool optimizer onto the Serve path#5543
tgrunnagle merged 4 commits into
mainfrom
vmcp-core_issue_5538

Conversation

@tgrunnagle

Copy link
Copy Markdown
Contributor

Summary

The tool optimizer (find_tool / call_tool) is currently a session-factory decorator with no equivalent on the Serve path. On the Serve path (s.core != nil), session registration sources the advertised capability set from core.ListTools, which bypasses the optimizer decorator entirely. This matters because #5445 (P3.2) makes Serve the single live path for all server.New callers — including the production thv vmcp serve --enable-optimizer path. The moment server.New routes through Serve, tools/list would advertise raw backend tools instead of find_tool / call_tool, silently breaking --enable-optimizer. This PR makes the Serve path optimizer-complete first so #5445 can flip the switch without regressing the optimizer.

  • Why: unblock P3.2 Reduce server.New body to the wrapper #5445 by closing the optimizer-on-Serve gap (#5442 deferred it; core/admission.go documents the invariant). Routing server.New through Serve for an optimizer-enabled config would otherwise silently break --enable-optimizer.
  • What: keep the optimizer a Serve-layer, session-scoped FTS5 index, but source its tool set from core.ListTools and route call_tool's inner invocation through core.CallTool by the real inner tool name.
  • Bonus: because core.CallTool authorizes the inner tool through the core admission seam, this also closes the deferred call_tool inner-target authorization gap documented in pkg/vmcp/core/admission.go — no separate optimizer-admission change needed.
  • Test-only behavior until P3.2 Reduce server.New body to the wrapper #5445 (no production composition root wires the Serve path yet); the legacy server.New path (s.core == nil) is unchanged.

Closes #5538

Blocks #5445. Relates to #5442 (the original deferral). Epic #5419 — vMCP interface refactor (RFC THV-0076).

Type of change

  • Refactoring (no behavior change)

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Added pkg/vmcp/server/serve_optimizer_test.go (internal package server) driving Serve with a stub core.VMCP and a fake optimizer factory:

  • tools/list advertises exactly {find_tool, call_tool} with raw tools hidden.
  • find_tool returns matches over the core's advertised set.
  • call_tool routes the inner invocation through core.CallTool with the real inner tool name.
  • A denied inner target returns a generic authorization denial (proves the inner-target authorization gap is closed).
  • Mismatched-caller identity binding is enforced on both meta-tools (session terminated).
  • Cross-pod lazyInjectSessionTools re-injects find_tool / call_tool for a rehydrated session.

The existing TestIntegration_SessionManagement_OptimizerMode continues to exercise the legacy server.New path (s.core == nil), unchanged until #5445 removes that path.

Changes

File Change
pkg/vmcp/server/serve_optimizer.go New. Serve-path optimizer wiring: serveSessionTools, optimizerSessionTools, and the find_tool / call_tool SDK handlers (identity binding, search over core tools, inner dispatch through core.CallTool).
pkg/vmcp/server/serve_optimizer_test.go New. Serve-level tests for advertisement, search, inner-target admission denial, identity binding, and cross-pod re-injection.
pkg/vmcp/server/serve.go Surface the resolved optimizer factory on *Server; document the optimizer caller contract (AdvertiseFromCore).
pkg/vmcp/server/serve_handlers.go injectCoreSessionCapabilities now calls serveSessionTools instead of coreSessionTools.
pkg/vmcp/server/server.go Add optimizerFactory field; cross-pod lazyInjectSessionTools now uses serveSessionTools on the Serve path.
pkg/vmcp/server/sessionmanager/factory.go Add FactoryConfig.AdvertiseFromCore; skip the factory's optimizer decorator when set to avoid double-indexing the shared store.
pkg/vmcp/server/sessionmanager/session_manager.go Retain the resolved optimizer factory on the Manager; expose it via OptimizerFactory().
pkg/vmcp/session/optimizerdec/decorator.go Extract the find_tool / call_tool definitions into the exported OptimizerTools() shared by both consumers.

Does this introduce a user-facing change?

No. This is test-only behavior until #5445 wires the Serve path into a production composition root. Once that lands, thv vmcp serve --enable-optimizer continues to advertise only find_tool / call_tool with no behavioral change for users.

Special notes for reviewers

  • The optimizer is deliberately kept out of the stateless core (it upserts a session's tools into a shared FTS5 store — transport/session state). AdvertiseFromCore is the single switch that prevents double-indexing: the session manager retains the factory's store/cleanup ownership but skips the decorator, and the Serve layer consumes the resolved factory directly.
  • call_tool returns the core handler's generic "call denied by authorization policy" result verbatim on a denied inner target, so no authorizer detail leaks.
  • One LOW defensive-parity finding from review was addressed: optimizerCallToolHandler guards against a (nil, nil) result from a future optimizer implementation.

Generated with Claude Code

Implements changes for issue #5538:
- Build a per-session optimizer over core.ListTools on the Serve path
  and advertise only find_tool/call_tool (sourced from the core)
- Route call_tool's inner invocation through core.CallTool by its real
  name, closing the deferred inner-target admission gap
- Skip the session factory's optimizer decorator on the Serve path
  (FactoryConfig.AdvertiseFromCore) to avoid double-indexing the store
- Surface the resolved optimizer factory via Manager.OptimizerFactory;
  store/cleanup ownership stays in sessionmanager

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/L Large PR: 600-999 lines changed label Jun 16, 2026
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.47059% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.88%. Comparing base (bc4bedd) to head (11bd32d).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pkg/vmcp/server/serve_optimizer.go 66.66% 15 Missing and 13 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5543      +/-   ##
==========================================
+ Coverage   69.85%   69.88%   +0.03%     
==========================================
  Files         647      649       +2     
  Lines       65796    66000     +204     
==========================================
+ Hits        45960    46125     +165     
- Misses      16491    16512      +21     
- Partials     3345     3363      +18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tgrunnagle tgrunnagle left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review summary

Strong, well-tested PR that delivers exactly what #5538 asks and closes the deferred call_tool inner-target authorization gap as a bonus. All 12 acceptance criteria are satisfied, and the four security invariants (optimizer-agnostic seam, real-inner-name authorization, generic no-leak denial, fail-closed identity binding) are correctly implemented and load-bearing-tested against the real coreToolHandler path. 0 HIGH findings — nothing blocks merge.

Two MEDIUM items worth attention (neither a live bug):

  • F1 find_tool dropped the nil-output guard the legacy + sibling call_tool handlers keep, while a comment still claims it "mirrors the legacy handler." Latent today; 2-line fix recommended in this PR.
  • F2 AdvertiseFromCore is an unenforced 3-point contract — enabling the optimizer but forgetting the flag silently double-indexes the FTS5 store. Acknowledged as unenforced until #5445; should be made fail-loud before that PR flips the switch.

Four LOW test/perf polish items are inline. Reviewed by 6 specialist agents (Security, Concurrency, Architecture, Go, Test-coverage, General). Codex cross-review skipped (CLI not installed).

Recommendation: COMMENT — land F1 here; track F2 as a #5445 prerequisite.

🤖 Generated with Claude Code

Comment thread pkg/vmcp/server/serve_optimizer.go
Comment thread pkg/vmcp/server/sessionmanager/factory.go
Comment thread pkg/vmcp/server/serve_optimizer.go
Comment thread pkg/vmcp/server/serve_optimizer.go
Comment thread pkg/vmcp/server/serve_optimizer_test.go Outdated
Comment thread pkg/vmcp/server/serve_optimizer_test.go
Addresses #5543 review comments:
- MEDIUM serve_optimizer.go (3428801576): add the output == nil guard to
  optimizerFindToolHandler for parity with the legacy/sibling handlers, so a
  nil result cannot marshal to "null" and surface as a success
- LOW serve_optimizer.go (3428801603): document that optimizerSessionTools
  re-upserts on every registration and cross-pod rehydration (idempotent by
  PK, repeated work not a leak; optimization deferred to #5445)
Addresses #5543 review comments:
- LOW serve_optimizer_test.go (3428801613): assert find_tool returns exactly
  the core's advertised tool names by decoding into FindToolOutput, instead of
  substring-matching the whole marshalled body; note ranking is out of scope
- LOW serve_optimizer.go (3428801607): add a test that optimizerToolHandler
  rejects an unknown meta-tool name, locking in the defensive default branch
- LOW serve_optimizer_test.go (3428801618): assert the cross-pod rehydration
  re-aggregates via a fresh core.ListTools and rebuilds the optimizer (the
  half of AC5 the test did not previously prove)
Addresses #5543 review comments:
- MEDIUM factory.go (3428801597): make AC6 structural instead of an unenforced
  contract. Manager surfaces the optimizer factory via OptimizerFactory() only
  when AdvertiseFromCore is true, so the session-factory decorator (used iff
  !AdvertiseFromCore) and the Serve-layer getter become mutually exclusive
  writers of the shared FTS5 store. A Serve composition root that enables the
  optimizer but forgets the flag now gets a nil factory (no Serve-layer
  optimizer) rather than a silent double-index.

Adds a test locking in the gate in both directions.
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 17, 2026
@tgrunnagle tgrunnagle marked this pull request as ready for review June 17, 2026 14:31
@tgrunnagle tgrunnagle merged commit fc70da7 into main Jun 17, 2026
45 checks passed
@tgrunnagle tgrunnagle deleted the vmcp-core_issue_5538 branch June 17, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire the tool optimizer onto the Serve path (sourced from the core)

2 participants