Skip to content

Commit c63b9eb

Browse files
Enforce Cedar policies on optimizer find_tool and call_tool (#4385)
* Enforce Cedar policies on optimizer find_tool and call_tool Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Enforce Cedar policies on optimizer call_tool via authz middleware Move Cedar authorization enforcement for the optimizer meta-tools out of the vmcp session layer and into the authz HTTP middleware, keeping the changes isolated to pkg/authz and the composition root (commands.go). The previous approach threaded an authorizers.Authorizer through five layers (incoming.go → commands.go → server.go → sessionmanager → factory → decorator), creating coupling between the optimizer decorator and the authorization system. New approach: - authz middleware intercepts tools/call for pass-through meta-tools: call_tool extracts the inner toolName from arguments and authorizes that backend tool; find_tool is allowed through as a discovery op. - commands.go builds passThroughTools (find_tool, call_tool) when the optimizer is enabled and passes it to NewIncomingAuthMiddleware. - incoming.go returns authzMw directly (as on main) instead of a raw authorizer; accepts passThroughTools to configure the middleware. - server.go, sessionmanager, and the optimizer decorator revert to their main-branch signatures with no knowledge of the authorizer. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Enforce Cedar policies on optimizer find_tool response find_tool calls were allowed through the authz middleware with no response filtering, letting callers discover tool names, descriptions, and schemas for tools they are not authorized to call. - Add filterFindToolResponse to ResponseFilteringWriter: intercepts the find_tool CallToolResult, identifies the FindToolOutput payload by attempting to unmarshal each TextContent item (stronger than checking the type string), filters output.Tools through Cedar policy, and populates the annotation cache from the unfiltered list so subsequent call_tool Cedar when-clauses evaluate correctly - Extend requiresResponseFiltering (renamed from isListOperation) and filterListResponse to dispatch find_tool responses through the new method, reusing all existing buffering, flush, and SSE logic - Update handleToolsCall to wrap find_tool with NewResponseFilteringWriter instead of passing through unfiltered - Unexport FilterToolsByPolicy and AuthorizeToolCall; move tool_filter_test to package authz to match - Expand TestFindToolResponseFilter with sub-tests covering Cedar filtering, isError pass-through, missing/non-FindToolOutput content pass-through, and annotation cache population from the unfiltered tool list Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 11cd266 commit c63b9eb

18 files changed

Lines changed: 2314 additions & 790 deletions

cmd/vmcp/app/commands.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
vmcprouter "github.com/stacklok/toolhive/pkg/vmcp/router"
4040
vmcpserver "github.com/stacklok/toolhive/pkg/vmcp/server"
4141
vmcpsession "github.com/stacklok/toolhive/pkg/vmcp/session"
42+
"github.com/stacklok/toolhive/pkg/vmcp/session/optimizerdec"
4243
vmcpstatus "github.com/stacklok/toolhive/pkg/vmcp/status"
4344
)
4445

@@ -506,13 +507,6 @@ func runServe(cmd *cobra.Command, _ []string) error {
506507

507508
slog.Info(fmt.Sprintf("Setting up incoming authentication (type: %s)", cfg.IncomingAuth.Type))
508509

509-
authMiddleware, authzMiddleware, authInfoHandler, err := factory.NewIncomingAuthMiddleware(ctx, cfg.IncomingAuth)
510-
if err != nil {
511-
return fmt.Errorf("failed to create authentication middleware: %w", err)
512-
}
513-
514-
slog.Info(fmt.Sprintf("Incoming authentication configured: %s", cfg.IncomingAuth.Type))
515-
516510
// Create server configuration with flags
517511
// Cobra validates flag types at parse time, so these values are safe to use directly
518512
host, _ := cmd.Flags().GetString("host")
@@ -582,6 +576,26 @@ func runServe(cmd *cobra.Command, _ []string) error {
582576
return err
583577
}
584578

579+
// When the optimizer is enabled, its meta-tools (find_tool, call_tool) must pass
580+
// through the authz response filter so they appear in tools/list. Authorization
581+
// for the underlying backend tools is enforced by the middleware's call_tool
582+
// interception. See: https://github.com/stacklok/toolhive/issues/4373
583+
var passThroughTools map[string]struct{}
584+
if optCfg != nil {
585+
passThroughTools = map[string]struct{}{
586+
optimizerdec.FindToolName: {},
587+
optimizerdec.CallToolName: {},
588+
}
589+
}
590+
591+
authMiddleware, authzMiddleware, authInfoHandler, err :=
592+
factory.NewIncomingAuthMiddleware(ctx, cfg.IncomingAuth, passThroughTools)
593+
if err != nil {
594+
return fmt.Errorf("failed to create authentication middleware: %w", err)
595+
}
596+
597+
slog.Info(fmt.Sprintf("Incoming authentication configured: %s", cfg.IncomingAuth.Type))
598+
585599
serverCfg := &vmcpserver.Config{
586600
Name: cfg.Name,
587601
Version: getVersion(),

docs/server/docs.go

Lines changed: 276 additions & 237 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 276 additions & 237 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)