Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ var _ = Describe("MCPExternalAuthConfig RedisStorageConfig CEL validation", func
ACLUserConfig: aclUserConfig,
},
shouldAdmit: false,
errMatch: "exactly one of addr or sentinelConfig must be set",
// DocPlanner fork: the rule also accepts the deprecated address alias.
errMatch: "exactly one of addr (or its deprecated alias address) or sentinelConfig must be set",
},
{
name: "both addr and sentinelConfig set",
Expand All @@ -117,7 +118,8 @@ var _ = Describe("MCPExternalAuthConfig RedisStorageConfig CEL validation", func
ACLUserConfig: aclUserConfig,
},
shouldAdmit: false,
errMatch: "exactly one of addr or sentinelConfig must be set",
// DocPlanner fork: the rule also accepts the deprecated address alias.
errMatch: "exactly one of addr (or its deprecated alias address) or sentinelConfig must be set",
},
{
name: "clusterMode without addr",
Expand Down
4 changes: 4 additions & 0 deletions deploy/charts/operator/tests/namespace_scope_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ suite: namespace-scoped install
# watched namespaces via WATCH_NAMESPACE, and the binding template must emit one
# RoleBinding per allowedNamespaces entry (and no ClusterRoleBinding). An empty
# allowlist must produce zero bindings rather than silently binding nothing.
# The fork names RBAC objects after the release (operator.rbacName), so pin the
# release name the manager-role assertion depends on.
release:
name: toolhive-operator
set:
operator.rbac.scope: namespace
operator.rbac.allowedNamespaces: [team-a, team-b]
Expand Down
15 changes: 7 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ require (
go.uber.org/zap v1.28.0
golang.ngrok.com/ngrok/v2 v2.1.4
golang.org/x/exp/jsonrpc2 v0.0.0-20260603202125-055de637280b
golang.org/x/mod v0.40.0
golang.org/x/mod v0.41.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.22.0
golang.org/x/term v0.45.0
golang.org/x/sync v0.23.0
golang.org/x/term v0.46.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.3
Expand All @@ -94,7 +94,6 @@ require (
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
)

Expand Down Expand Up @@ -295,8 +294,8 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.ngrok.com/muxado/v2 v2.0.1 // indirect
golang.org/x/exp/event v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
Expand Down Expand Up @@ -348,9 +347,9 @@ require (
go.opentelemetry.io/otel v1.45.0
go.opentelemetry.io/otel/metric v1.45.0
go.opentelemetry.io/otel/trace v1.45.0
golang.org/x/crypto v0.56.0
golang.org/x/crypto v0.57.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/sys v0.47.0
golang.org/x/sys v0.48.0
k8s.io/client-go v0.35.3
)

Expand Down
452 changes: 69 additions & 383 deletions go.sum

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions pkg/authserver/storage/cimd_decorator.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,9 @@ func (d *CIMDStorageDecorator) fetch(ctx context.Context, id string) (fosite.Cli
// When ScopesSupported is configured:
// - Declared scopes are validated via registration.ValidateScopes (same
// function as the DCR handler).
// - Omitted scope uses ValidateScopes(nil, scopesSupported) which returns
// DefaultScopes when DefaultScopes ⊆ ScopesSupported, matching DCR.
// If DefaultScopes ⊄ ScopesSupported the document must declare scope
// explicitly to avoid ambiguous privilege grant.
// - Omitted scope uses ValidateScopes(nil, scopesSupported), matching DCR.
// In the DocPlanner fork (16c2fba5e) that returns the advertised
// ScopesSupported set as the default grant, so it does not fail.
// When ScopesSupported is not configured: no AS-level validation; declared
// scopes are used directly, or nil to let buildFositeClient apply DefaultScopes.
// In both cases BaselineClientScopes is unioned in after validation,
Expand All @@ -177,7 +176,7 @@ func (d *CIMDStorageDecorator) fetch(ctx context.Context, id string) (fosite.Cli
}
resolvedScopes = computed
} else {
// Omitted scope: match DCR — give DefaultScopes when they fit, else require explicit scope.
// Omitted scope: match DCR, which grants the advertised ScopesSupported set.
computed, dcrErr := registration.ValidateScopes(nil, d.scopesSupported)
if dcrErr != nil {
return nil, fmt.Errorf("%w: CIMD document at %s omits scope but "+
Expand Down
6 changes: 4 additions & 2 deletions pkg/authserver/storage/cimd_decorator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,12 @@ func TestFetch_ScopeResolution(t *testing.T) {
wantScopes: registration.DefaultScopes,
},
{
name: "omitted scope with restrictive ScopesSupported requires explicit scope",
// DocPlanner fork (16c2fba5e): an omitted scope defaults to the
// advertised ScopesSupported set, the same as DCR.
name: "omitted scope with restrictive ScopesSupported uses ScopesSupported",
docScope: "",
scopesSupported: []string{"openid"},
wantErr: true,
wantScopes: []string{"openid"},
},
{
name: "baseline unioned into scope set",
Expand Down
3 changes: 3 additions & 0 deletions pkg/vmcp/server/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ func Serve(ctx context.Context, v core.VMCP, cfg *ServerConfig) (*Server, error)
// built directly in Serve from ServerConfig.SessionManagerConfig (a pre-built
// *sessionmanager.FactoryConfig that carries the session factory and optimizer
// wiring), not via Config→New, so these Config fields are unused on the Serve path.
// - WriteTimeout, SessionOwnerAdvertiseURL: DocPlanner fork fields. The fork's vMCP
// CLI builds the server through server.New with a full Config, never through
// Serve, so ServerConfig does not carry them.
func buildServeConfig(cfg *ServerConfig) *Config {
return &Config{
Name: cmp.Or(cfg.Name, defaultServerName),
Expand Down
3 changes: 3 additions & 0 deletions pkg/vmcp/server/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ func TestBuildServeConfigMapsSharedFields(t *testing.T) {
"SessionFactory": {}, // session manager built in Serve from ServerConfig.SessionManagerConfig
"OptimizerFactory": {}, // optimizer wiring carried on ServerConfig.SessionManagerConfig (FactoryConfig)
"OptimizerConfig": {}, // optimizer wiring carried on ServerConfig.SessionManagerConfig (FactoryConfig)
// DocPlanner fork fields: the fork's CLI uses server.New, not Serve.
"WriteTimeout": {},
"SessionOwnerAdvertiseURL": {},
}

// Every field set to a non-zero value so a dropped mapping surfaces as a zero
Expand Down
9 changes: 8 additions & 1 deletion pkg/vmcp/server/sessionmanager/session_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (s *refreshableTestSession) CreatorIdentity() *auth.Identity {
type configurableFailDataStorage struct {
transportsession.DataStorage
storeCallCount int
failStoreAfter int // fail Upsert/Create after this many successful calls (0 = never fail, -1 = always fail)
failStoreAfter int // fail Upsert/Create/Update after this many successful calls (0 = never fail, -1 = always fail)
failDelete bool
}

Expand All @@ -140,6 +140,13 @@ func (s *configurableFailDataStorage) Create(ctx context.Context, id string, met
return s.DataStorage.Create(ctx, id, metadata)
}

func (s *configurableFailDataStorage) Update(ctx context.Context, id string, metadata map[string]string) (bool, error) {
if s.shouldFail() {
return false, errors.New("injected Update failure")
}
return s.DataStorage.Update(ctx, id, metadata)
}

func (s *configurableFailDataStorage) Delete(ctx context.Context, id string) error {
if s.failDelete {
return errors.New("injected Delete failure")
Expand Down
10 changes: 9 additions & 1 deletion pkg/vmcp/session/restore_caller_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,16 @@ func restoreTestBackends() []*vmcp.Backend {
return []*vmcp.Backend{{ID: "memory"}}
}

// callerIdentity carries the iss and sub claims that the (iss, sub) session
// binding is derived from.
func callerIdentity(subject, token string) *auth.Identity {
return &auth.Identity{PrincipalInfo: auth.PrincipalInfo{Subject: subject}, Token: token}
return &auth.Identity{
PrincipalInfo: auth.PrincipalInfo{
Subject: subject,
Claims: map[string]any{"iss": "https://issuer.example.com", "sub": subject},
},
Token: token,
}
}

func storedBoundSession(t *testing.T, factory MultiSessionFactory) map[string]string {
Expand Down
9 changes: 5 additions & 4 deletions pkg/webhook/validating/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ func TestValidatingMiddleware_RequestBodySizeLimit(t *testing.T) {

body := makeJSONBodyOfSize(t, webhook.MaxRequestSize+1)
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
ctx := context.WithValue(req.Context(), mcp.MCPRequestContextKey, &mcp.ParsedMCPRequest{Method: "tools/call", ID: 1})
ctx := context.WithValue(req.Context(), mcp.MCPRequestContextKey, &mcp.ParsedMCPRequest{Method: "tools/call", ID: 1, IsRequest: true})
req = req.WithContext(ctx)

var nextCalled bool
Expand Down Expand Up @@ -374,7 +374,7 @@ func TestValidatingMiddleware_RequestBodySizeLimit(t *testing.T) {

body := makeJSONBodyOfSize(t, webhook.MaxRequestSize)
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
ctx := context.WithValue(req.Context(), mcp.MCPRequestContextKey, &mcp.ParsedMCPRequest{Method: "tools/call", ID: 1})
ctx := context.WithValue(req.Context(), mcp.MCPRequestContextKey, &mcp.ParsedMCPRequest{Method: "tools/call", ID: 1, IsRequest: true})
req = req.WithContext(ctx)

var nextCalled bool
Expand Down Expand Up @@ -541,8 +541,9 @@ func TestCreateMiddleware_ResolvesHMACSecret(t *testing.T) {

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader([]byte(`{"jsonrpc":"2.0","method":"tools/call","id":1}`)))
req = req.WithContext(context.WithValue(req.Context(), mcp.MCPRequestContextKey, &mcp.ParsedMCPRequest{
Method: "tools/call",
ID: 1,
Method: "tools/call",
ID: 1,
IsRequest: true,
}))

rr := httptest.NewRecorder()
Expand Down
Loading