Repository: crossplane/crossplane

Issue:
[v2] Crossplane v2 prototype allows a namespaced XR to compose cluster scoped resources
<!--
Thank you for helping to improve Crossplane!

Please be sure to search for open issues before raising a new one. We use issues
for bug reports and feature requests. Please find us at https://slack.crossplane.io
for questions, support, and discussion.
-->

### What happened?
<!--
Please let us know what behaviour you expected and how Crossplane diverged from
that behaviour.
-->

In the Crossplane v2 prototype a namespaced XR shouldn't be allowed to compose cluster scoped resources. This is because Kubernetes doesn't allow a cluster scoped resource to be owned by (i.e. have owner references to) a namespaced resource. We recommend using cluster scoped XRs to compose cluster scoped (and/or namespaced) resources.

Currently the prototype will let you compose cluster scoped resources. It'll successfully create them, but they won't have owner references back to the XR. This means they won't be deleted when the XR is deleted. We use Kubernetes garbage collection, which relies on owner references, to delete composed resources when an XR is deleted.

### How can we reproduce it?
<!--
Help us to reproduce your bug as succinctly and precisely as possible. Artifacts
such as example manifests or a script that triggers the issue are highly
appreciated!
-->

Create a namespaced XR and use a composition that composes cluster scoped resources.

### What environment did it happen in?
Crossplane version: [22f775c](https://github.com/crossplane/crossplane/commit/22f775cb6571838f816236d326cfc0b6ee1dba1f)

<!--
Include at least the version or commit of Crossplane you were running. Consider
also including your:

* Cloud provider or hardware configuration
* Kubernetes version (use `kubectl version`)
* Kubernetes distribution (e.g. Tectonic, GKE, OpenShift)
* OS (e.g. from /etc/os-release)
* Kernel (e.g. `uname -a`)
-->


Hints from issue comments:
@negz: I think this is fairly high priority to address. I worry that folk'll compose cluster scoped resources (e.g. legacy MRs) without knowing they shouldn't. If they take a dependency on the behavior we'll likely be forced to support it.@yehlo: Did think about this for a while, do you have any suggestion how this might be solved? 
I'm new to the code of Crossplane so this might be wrong.

As I understand it you would want to prohibit the creation of cluster scoped resources through a composition that reconciles a namespace scoped XR. 

Since the users could use any function to generate the manifests to create, we would have to render the output of the function beforehand and make sure if the XR has `metadata.namespace` defined any rendered manifests also have `metadata.namespace` defined.

Is this the direction you wanted to take?
If so I think here might be the right place to try and block this from happening: https://github.com/crossplane/crossplane/blob/v2/internal/controller/apiextensions/composite/composition_functions.go#L328 

Though this would lead to the resource having been created already (I think?). 

Could additionally mirror what's done in the Crossplane render command in a validation hook so that a user will be blocked to even apply a composition that introduces such behaviour.@negz: Yeah, I haven't had a chance to think much about this but I assume you're right. We'd need to:

1. Render all resources
2. Check whether any of the resulting resources appeared to be cluster scoped (despite a namespaced XR)
3. Return an error and refuse to apply them
@lsviben: I can grab this one next if nobody is working on it

Solution patch:
diff --git a/.golangci.yml b/.golangci.yml
index 5a8ffec62..ef8fe57c4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -24,6 +24,7 @@ linters:
     # This isn't a widely accepted Go best practice, and would be laborious to
     # apply to existing code.
     - wsl
+    - wsl_v5
     - nlreturn
 
     # Warns about uses of fmt.Sprintf that are less performant than alternatives
diff --git a/apis/pkg/v1/function_types.go b/apis/pkg/v1/function_types.go
index 6b31c3dc1..47aa0cb52 100644
--- a/apis/pkg/v1/function_types.go
+++ b/apis/pkg/v1/function_types.go
@@ -105,7 +105,8 @@ type FunctionRevision struct {
 
 // FunctionRevisionStatus represents the observed state of a FunctionRevision.
 type FunctionRevisionStatus struct {
-	PackageRevisionStatus `json:",inline"`
+	PackageRevisionStatus        `json:",inline"`
+	PackageRevisionRuntimeStatus `json:",inline"`
 
 	// Endpoint is the gRPC endpoint where Crossplane will send
 	// RunFunctionRequests.
diff --git a/apis/pkg/v1/interfaces.go b/apis/pkg/v1/interfaces.go
index 2881be2b1..6bc7f7c3b 100644
--- a/apis/pkg/v1/interfaces.go
+++ b/apis/pkg/v1/interfaces.go
@@ -474,8 +474,14 @@ type PackageRevisionWithRuntime interface { //nolint:interfacebloat // TODO(negz
 	GetTLSServerSecretName() *string
 	SetTLSServerSecretName(n *string)
 
+	GetObservedTLSServerSecretName() *string
+	SetObservedTLSServerSecretName(n *string)
+
 	GetTLSClientSecretName() *string
 	SetTLSClientSecretName(n *string)
+
+	GetObservedTLSClientSecretName() *string
+	SetObservedTLSClientSecretName(n *string)
 }
 
 // SetAppliedImageConfigRefs sets applied image config refs, replacing any
@@ -685,6 +691,16 @@ func (p *ProviderRevision) SetTLSServerSecretName(s *string) {
 	p.Spec.TLSServerSecretName = s
 }
 
+// GetObservedTLSServerSecretName of this ProviderRevision.
+func (p *ProviderRevision) GetObservedTLSServerSecretName() *string {
+	return p.Status.TLSServerSecretName
+}
+
+// SetObservedTLSServerSecretName of this ProviderRevision.
+func (p *ProviderRevision) SetObservedTLSServerSecretName(s *string) {
+	p.Status.TLSServerSecretName = s
+}
+
 // GetTLSClientSecretName of this ProviderRevision.
 func (p *ProviderRevision) GetTLSClientSecretName() *string {
 	return p.Spec.TLSClientSecretName
@@ -695,6 +711,16 @@ func (p *ProviderRevision) SetTLSClientSecretName(s *string) {
 	p.Spec.TLSClientSecretName = s
 }
 
+// GetObservedTLSClientSecretName of this ProviderRevision.
+func (p *ProviderRevision) GetObservedTLSClientSecretName() *string {
+	return p.Status.TLSClientSecretName
+}
+
+// SetObservedTLSClientSecretName of this ProviderRevision.
+func (p *ProviderRevision) SetObservedTLSClientSecretName(s *string) {
+	p.Status.TLSClientSecretName = s
+}
+
 // GetCommonLabels of this ProviderRevision.
 func (p *ProviderRevision) GetCommonLabels() map[string]string {
 	return p.Spec.CommonLabels
@@ -1215,11 +1241,31 @@ func (r *FunctionRevision) SetTLSServerSecretName(s *string) {
 	r.Spec.TLSServerSecretName = s
 }
 
+// GetObservedTLSServerSecretName of this FunctionRevision.
+func (r *FunctionRevision) GetObservedTLSServerSecretName() *string {
+	return r.Status.TLSServerSecretName
+}
+
+// SetObservedTLSServerSecretName of this FunctionRevision.
+func (r *FunctionRevision) SetObservedTLSServerSecretName(s *string) {
+	r.Status.TLSServerSecretName = s
+}
+
 // GetTLSClientSecretName of this FunctionRevision.
 func (r *FunctionRevision) GetTLSClientSecretName() *string {
 	return r.Spec.TLSClientSecretName
 }
 
+// SetObservedTLSClientSecretName of this FunctionRevision.
+func (r *FunctionRevision) SetObservedTLSClientSecretName(s *string) {
+	r.Status.TLSClientSecretName = s
+}
+
+// GetObservedTLSClientSecretName of this FunctionRevision.
+func (r *FunctionRevision) GetObservedTLSClientSecretName() *string {
+	return r.Status.TLSClientSecretName
+}
+
 // SetTLSClientSecretName of this FunctionRevision.
 func (r *FunctionRevision) SetTLSClientSecretName(s *string) {
 	r.Spec.TLSClientSecretName = s
diff --git a/apis/pkg/v1/package_runtime_types.go b/apis/pkg/v1/package_runtime_types.go
index 76fca8e5b..db452a88d 100644
--- a/apis/pkg/v1/package_runtime_types.go
+++ b/apis/pkg/v1/package_runtime_types.go
@@ -44,6 +44,20 @@ type PackageRevisionRuntimeSpec struct {
 	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
 }
 
+// PackageRevisionRuntimeStatus represents the observed state of a package
+// revision with a runtime.
+type PackageRevisionRuntimeStatus struct {
+	// TLSServerSecretName is the name of the TLS Secret that stores server
+	// certificates of the Provider.
+	// +optional
+	TLSServerSecretName *string `json:"tlsServerSecretName,omitempty"`
+
+	// TLSClientSecretName is the name of the TLS Secret that stores client
+	// certificates of the Provider.
+	// +optional
+	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
+}
+
 // A RuntimeConfigReference to a runtime config resource that will be used
 // to configure the package runtime.
 type RuntimeConfigReference struct {
diff --git a/apis/pkg/v1/provider_types.go b/apis/pkg/v1/provider_types.go
index bf3ff6eb1..d3d471f8f 100644
--- a/apis/pkg/v1/provider_types.go
+++ b/apis/pkg/v1/provider_types.go
@@ -75,6 +75,12 @@ type ProviderRevisionSpec struct {
 	PackageRevisionRuntimeSpec `json:",inline"`
 }
 
+// ProviderRevisionStatus represents the observed state of a ProviderRevision.
+type ProviderRevisionStatus struct {
+	PackageRevisionStatus        `json:",inline"`
+	PackageRevisionRuntimeStatus `json:",inline"`
+}
+
 // +kubebuilder:object:root=true
 // +genclient
 // +genclient:nonNamespaced
@@ -99,8 +105,8 @@ type ProviderRevision struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
-	Spec   ProviderRevisionSpec  `json:"spec,omitempty"`
-	Status PackageRevisionStatus `json:"status,omitempty"`
+	Spec   ProviderRevisionSpec   `json:"spec,omitempty"`
+	Status ProviderRevisionStatus `json:"status,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/apis/pkg/v1/zz_generated.deepcopy.go b/apis/pkg/v1/zz_generated.deepcopy.go
index 2dc86c7df..b14dfbd00 100644
--- a/apis/pkg/v1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1/zz_generated.deepcopy.go
@@ -331,6 +331,7 @@ func (in *FunctionRevisionSpec) DeepCopy() *FunctionRevisionSpec {
 func (in *FunctionRevisionStatus) DeepCopyInto(out *FunctionRevisionStatus) {
 	*out = *in
 	in.PackageRevisionStatus.DeepCopyInto(&out.PackageRevisionStatus)
+	in.PackageRevisionRuntimeStatus.DeepCopyInto(&out.PackageRevisionRuntimeStatus)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionStatus.
@@ -418,6 +419,31 @@ func (in *PackageRevisionRuntimeSpec) DeepCopy() *PackageRevisionRuntimeSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageRevisionRuntimeStatus) DeepCopyInto(out *PackageRevisionRuntimeStatus) {
+	*out = *in
+	if in.TLSServerSecretName != nil {
+		in, out := &in.TLSServerSecretName, &out.TLSServerSecretName
+		*out = new(string)
+		**out = **in
+	}
+	if in.TLSClientSecretName != nil {
+		in, out := &in.TLSClientSecretName, &out.TLSClientSecretName
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageRevisionRuntimeStatus.
+func (in *PackageRevisionRuntimeStatus) DeepCopy() *PackageRevisionRuntimeStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageRevisionRuntimeStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) {
 	*out = *in
@@ -713,6 +739,23 @@ func (in *ProviderRevisionSpec) DeepCopy() *ProviderRevisionSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ProviderRevisionStatus) DeepCopyInto(out *ProviderRevisionStatus) {
+	*out = *in
+	in.PackageRevisionStatus.DeepCopyInto(&out.PackageRevisionStatus)
+	in.PackageRevisionRuntimeStatus.DeepCopyInto(&out.PackageRevisionRuntimeStatus)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderRevisionStatus.
+func (in *ProviderRevisionStatus) DeepCopy() *ProviderRevisionStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ProviderRevisionStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
 	*out = *in
diff --git a/apis/pkg/v1beta1/zz_generated.deepcopy.go b/apis/pkg/v1beta1/zz_generated.deepcopy.go
index 32d1cb034..a18618477 100644
--- a/apis/pkg/v1beta1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1beta1/zz_generated.deepcopy.go
@@ -396,6 +396,7 @@ func (in *FunctionRevisionSpec) DeepCopy() *FunctionRevisionSpec {
 func (in *FunctionRevisionStatus) DeepCopyInto(out *FunctionRevisionStatus) {
 	*out = *in
 	in.PackageRevisionStatus.DeepCopyInto(&out.PackageRevisionStatus)
+	in.PackageRevisionRuntimeStatus.DeepCopyInto(&out.PackageRevisionRuntimeStatus)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionStatus.
@@ -855,6 +856,31 @@ func (in *PackageRevisionRuntimeSpec) DeepCopy() *PackageRevisionRuntimeSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageRevisionRuntimeStatus) DeepCopyInto(out *PackageRevisionRuntimeStatus) {
+	*out = *in
+	if in.TLSServerSecretName != nil {
+		in, out := &in.TLSServerSecretName, &out.TLSServerSecretName
+		*out = new(string)
+		**out = **in
+	}
+	if in.TLSClientSecretName != nil {
+		in, out := &in.TLSClientSecretName, &out.TLSClientSecretName
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageRevisionRuntimeStatus.
+func (in *PackageRevisionRuntimeStatus) DeepCopy() *PackageRevisionRuntimeStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageRevisionRuntimeStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) {
 	*out = *in
diff --git a/apis/pkg/v1beta1/zz_generated.function_types.go b/apis/pkg/v1beta1/zz_generated.function_types.go
index 7f582dcee..d810a9626 100644
--- a/apis/pkg/v1beta1/zz_generated.function_types.go
+++ b/apis/pkg/v1beta1/zz_generated.function_types.go
@@ -105,7 +105,8 @@ type FunctionRevision struct {
 
 // FunctionRevisionStatus represents the observed state of a FunctionRevision.
 type FunctionRevisionStatus struct {
-	PackageRevisionStatus `json:",inline"`
+	PackageRevisionStatus        `json:",inline"`
+	PackageRevisionRuntimeStatus `json:",inline"`
 
 	// Endpoint is the gRPC endpoint where Crossplane will send
 	// RunFunctionRequests.
diff --git a/apis/pkg/v1beta1/zz_generated.package_runtime_types.go b/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
index 0487443ba..433949944 100644
--- a/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
+++ b/apis/pkg/v1beta1/zz_generated.package_runtime_types.go
@@ -46,6 +46,20 @@ type PackageRevisionRuntimeSpec struct {
 	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
 }
 
+// PackageRevisionRuntimeStatus represents the observed state of a package
+// revision with a runtime.
+type PackageRevisionRuntimeStatus struct {
+	// TLSServerSecretName is the name of the TLS Secret that stores server
+	// certificates of the Provider.
+	// +optional
+	TLSServerSecretName *string `json:"tlsServerSecretName,omitempty"`
+
+	// TLSClientSecretName is the name of the TLS Secret that stores client
+	// certificates of the Provider.
+	// +optional
+	TLSClientSecretName *string `json:"tlsClientSecretName,omitempty"`
+}
+
 // A RuntimeConfigReference to a runtime config resource that will be used
 // to configure the package runtime.
 type RuntimeConfigReference struct {
diff --git a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
index a826dcd92..04ad03f80 100644
--- a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
+++ b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
@@ -297,6 +297,16 @@ spec:
                   different from spec.image if the package path was rewritten using an
                   image config.
                 type: string
+              tlsClientSecretName:
+                description: |-
+                  TLSClientSecretName is the name of the TLS Secret that stores client
+                  certificates of the Provider.
+                type: string
+              tlsServerSecretName:
+                description: |-
+                  TLSServerSecretName is the name of the TLS Secret that stores server
+                  certificates of the Provider.
+                type: string
             type: object
         type: object
     served: true
@@ -583,6 +593,16 @@ spec:
                   different from spec.image if the package path was rewritten using an
                   image config.
                 type: string
+              tlsClientSecretName:
+                description: |-
+                  TLSClientSecretName is the name of the TLS Secret that stores client
+                  certificates of the Provider.
+                type: string
+              tlsServerSecretName:
+                description: |-
+                  TLSServerSecretName is the name of the TLS Secret that stores server
+                  certificates of the Provider.
+                type: string
             type: object
         type: object
     served: true
diff --git a/cluster/crds/pkg.crossplane.io_providerrevisions.yaml b/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
index c1f5c967b..de7dd9732 100644
--- a/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
+++ b/cluster/crds/pkg.crossplane.io_providerrevisions.yaml
@@ -180,8 +180,8 @@ spec:
             - revision
             type: object
           status:
-            description: PackageRevisionStatus represents the observed state of a
-              PackageRevision.
+            description: ProviderRevisionStatus represents the observed state of a
+              ProviderRevision.
             properties:
               appliedImageConfigRefs:
                 description: |-
@@ -292,6 +292,16 @@ spec:
                   different from spec.image if the package path was rewritten using an
                   image config.
                 type: string
+              tlsClientSecretName:
+                description: |-
+                  TLSClientSecretName is the name of the TLS Secret that stores client
+                  certificates of the Provider.
+                type: string
+              tlsServerSecretName:
+                description: |-
+                  TLSServerSecretName is the name of the TLS Secret that stores server
+                  certificates of the Provider.
+                type: string
             type: object
         type: object
     served: true
diff --git a/cmd/crossplane/core/core.go b/cmd/crossplane/core/core.go
index 0228398fc..314f5a4d5 100644
--- a/cmd/crossplane/core/core.go
+++ b/cmd/crossplane/core/core.go
@@ -49,6 +49,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/ratelimiter"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
 
+	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/internal/controller/apiextensions"
 	apiextensionscontroller "github.com/crossplane/crossplane/internal/controller/apiextensions/controller"
 	"github.com/crossplane/crossplane/internal/controller/pkg"
@@ -96,7 +97,7 @@ type startCommand struct {
 
 	XpkgCacheDir string `aliases:"cache-dir" default:"/cache/xpkg" env:"XPKG_CACHE_DIR,CACHE_DIR" help:"Directory used for caching package images." short:"c"`
 
-	PackageRuntime string `default:"Deployment" env:"PACKAGE_RUNTIME" help:"The package runtime to use for packages with a runtime (e.g. Providers and Functions)"`
+	PackageRuntime string `default:"Deployment" env:"PACKAGE_RUNTIME" help:"The package runtime to use for packages with a runtime (e.g. Providers and Functions)" placeholder:"runtime | runtime1=package1;runtime2=package2"`
 
 	SyncInterval                     time.Duration `default:"1h"  help:"How often all resources will be double-checked for drift from the desired state."                      short:"s"`
 	PollInterval                     time.Duration `default:"1m"  help:"How often individual resources will be checked for drift from the desired state."`
@@ -413,17 +414,27 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
 		return errors.Wrap(err, "cannot setup API extension controllers")
 	}
 
-	var pr pkgcontroller.PackageRuntime
-	switch c.PackageRuntime {
-	case string(pkgcontroller.PackageRuntimeDeployment):
-		pr = pkgcontroller.PackageRuntimeDeployment
-	case string(pkgcontroller.PackageRuntimeExternal):
-		pr = pkgcontroller.PackageRuntimeExternal
+	var pr pkgcontroller.ActiveRuntime
+	switch rt := pkgcontroller.PackageRuntime(c.PackageRuntime); rt {
+	case pkgcontroller.PackageRuntimeUnspecified:
+		rt = pkgcontroller.PackageRuntimeDeployment
+		fallthrough
+	case pkgcontroller.PackageRuntimeDeployment, pkgcontroller.PackageRuntimeExternal:
+		pr = pkgcontroller.NewActiveRuntime(
+			pkgcontroller.WithDefaultPackageRuntime(rt),
+		)
 	default:
-		return errors.Errorf("unsupported package runtime %q, supported runtimes are %q and %q",
-			c.PackageRuntime, pkgcontroller.PackageRuntimeDeployment, pkgcontroller.PackageRuntimeExternal)
+		pr, err = pkgcontroller.ParsePackageRuntime(string(rt))
+		if err != nil {
+			return errors.Errorf("unsupported package runtime %q, supported runtimes are [%q, %q]",
+				c.PackageRuntime, pkgcontroller.PackageRuntimeDeployment,
+				pkgcontroller.PackageRuntimeExternal)
+		}
 	}
 
+	log.Info("Package Runtime for Provider: " + string(pr.For(pkgv1.ProviderKind)))
+	log.Info("Package Runtime for Function: " + string(pr.For(pkgv1.FunctionKind)))
+
 	po := pkgcontroller.Options{
 		Options:                          o,
 		Cache:                            xpkg.NewFsPackageCache(c.XpkgCacheDir, afero.NewOsFs()),
diff --git a/internal/controller/apiextensions/composite/composition_functions.go b/internal/controller/apiextensions/composite/composition_functions.go
index 96f187f46..334160932 100644
--- a/internal/controller/apiextensions/composite/composition_functions.go
+++ b/internal/controller/apiextensions/composite/composition_functions.go
@@ -74,20 +74,22 @@ const (
 	errGetComposed              = "cannot get composed resource"
 	errMarshalJSON              = "cannot marshal to JSON"
 
-	errFmtApplyCD                    = "cannot apply composed resource %q"
-	errFmtFetchCDConnectionDetails   = "cannot fetch connection details for composed resource %q (a %s named %s)"
-	errFmtUnmarshalPipelineStepInput = "cannot unmarshal input for Composition pipeline step %q"
-	errFmtGetCredentialsFromSecret   = "cannot get Composition pipeline step %q credential %q from Secret"
-	errFmtRunPipelineStep            = "cannot run Composition pipeline step %q"
-	errFmtControllerMismatch         = "refusing to delete composed resource %q that is controlled by %s %q"
-	errFmtCleanupLabelsCD            = "cannot cleanup composed resource labels of resource %q (a %s named %s)"
-	errFmtDeleteCD                   = "cannot delete composed resource %q (a %s named %s)"
-	errFmtUnmarshalDesiredCD         = "cannot unmarshal desired composed resource %q from RunFunctionResponse"
-	errFmtRenderMetadata             = "cannot render metadata for composed resource %q"
-	errFmtGenerateName               = "cannot generate a name for composed resource %q"
-	errFmtCDAsStruct                 = "cannot encode composed resource %q to protocol buffer Struct well-known type"
-	errFmtFatalResult                = "pipeline step %q returned a fatal result: %s"
-	errFmtInvalidName                = "cannot apply composed resource %q because it has an invalid name %q. Must be a valid RFC 1123 subdomain name."
+	errFmtApplyCD                     = "cannot apply composed resource %q"
+	errFmtFetchCDConnectionDetails    = "cannot fetch connection details for composed resource %q (a %s named %s)"
+	errFmtUnmarshalPipelineStepInput  = "cannot unmarshal input for Composition pipeline step %q"
+	errFmtGetCredentialsFromSecret    = "cannot get Composition pipeline step %q credential %q from Secret"
+	errFmtRunPipelineStep             = "cannot run Composition pipeline step %q"
+	errFmtControllerMismatch          = "refusing to delete composed resource %q that is controlled by %s %q"
+	errFmtCleanupLabelsCD             = "cannot cleanup composed resource labels of resource %q (a %s named %s)"
+	errFmtDeleteCD                    = "cannot delete composed resource %q (a %s named %s)"
+	errFmtUnmarshalDesiredCD          = "cannot unmarshal desired composed resource %q from RunFunctionResponse"
+	errFmtRenderMetadata              = "cannot render metadata for composed resource %q"
+	errFmtGenerateName                = "cannot generate a name for composed resource %q"
+	errFmtCDAsStruct                  = "cannot encode composed resource %q to protocol buffer Struct well-known type"
+	errFmtFatalResult                 = "pipeline step %q returned a fatal result: %s"
+	errFmtInvalidName                 = "cannot apply composed resource %q because it has an invalid name %q. Must be a valid RFC 1123 subdomain name."
+	errFmtGetResourceMapping          = "cannot check if composed resource %q is namespaced (a %s named %s)"
+	errFmtNamespacedXRClusterResource = "cannot apply cluster scoped composed resource %q (a %s named %s) for a namespaced composite resource."
 )
 
 // Server-side-apply field owners. We need two of these because it's possible
@@ -436,6 +438,24 @@ func (c *FunctionComposer) Compose(ctx context.Context, xr *composite.Unstructur
 			cd.SetName(or.Resource.GetName())
 		}
 
+		// If the XR is namespaced then the composed resource must be too.
+		// NOTE(lsviben): We only check resources which do not have a namespace
+		// set, although the namespace for all resources composed by a
+		// namespaced XR is set in the RenderComposedResourceMetadata
+		// step. But as in the previous step we set the namespace of observed
+		// resources, we are effectively checking only once per resource,
+		// before we create it.
+		if xr.GetNamespace() != "" && cd.GetNamespace() == "" {
+			isNs, err := c.client.IsObjectNamespaced(cd)
+			if err != nil {
+				return CompositionResult{}, errors.Wrapf(err, errFmtGetResourceMapping, name, cd.GetKind(), cd.GetName())
+			}
+
+			if !isNs {
+				return CompositionResult{}, errors.Errorf(errFmtNamespacedXRClusterResource, name, cd.GetKind(), cd.GetName())
+			}
+		}
+
 		// Set standard composed resource metadata that is derived from the XR.
 		if err := RenderComposedResourceMetadata(cd, xr, ResourceName(name)); err != nil {
 			return CompositionResult{}, errors.Wrapf(err, errFmtRenderMetadata, name)
diff --git a/internal/controller/pkg/controller/options.go b/internal/controller/pkg/controller/options.go
index d264ee19b..f2fca2f03 100644
--- a/internal/controller/pkg/controller/options.go
+++ b/internal/controller/pkg/controller/options.go
@@ -41,7 +41,7 @@ type Options struct {
 	FetcherOptions []xpkg.FetcherOpt
 
 	// PackageRuntime specifies the runtime to use for package runtime.
-	PackageRuntime PackageRuntime
+	PackageRuntime ActiveRuntime
 
 	// MaxConcurrentPackageEstablishers is the maximum number of goroutines to use
 	// for establishing Providers, Configurations and Functions.
diff --git a/internal/controller/pkg/controller/runtime.go b/internal/controller/pkg/controller/runtime.go
index 8419634dd..2377e7e9a 100644
--- a/internal/controller/pkg/controller/runtime.go
+++ b/internal/controller/pkg/controller/runtime.go
@@ -1,16 +1,129 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
 package controller
 
+import (
+	"strings"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+)
+
+// ActiveRuntime is the runtime to use for packages with runtime.
+type ActiveRuntime struct {
+	runtimes       map[PackageKind]PackageRuntime
+	defaultRuntime PackageRuntime
+}
+
+// RuntimeOption defines options to build up an active runtime.
+type RuntimeOption func(runtime *ActiveRuntime)
+
+// WithDefaultPackageRuntime marks a default runtime for unset kinds.
+func WithDefaultPackageRuntime(runtime PackageRuntime) RuntimeOption {
+	return func(ar *ActiveRuntime) {
+		ar.defaultRuntime = runtime
+	}
+}
+
+// WithPackageRuntime associates a runtime to a kind.
+func WithPackageRuntime(kind PackageKind, runtime PackageRuntime) RuntimeOption {
+	return func(ar *ActiveRuntime) {
+		ar.runtimes[kind] = runtime
+	}
+}
+
+// NewActiveRuntime builds an ActiveRuntime based on the provided options.
+func NewActiveRuntime(o ...RuntimeOption) ActiveRuntime {
+	r := ActiveRuntime{
+		runtimes: make(map[PackageKind]PackageRuntime),
+	}
+	for _, o := range o {
+		o(&r)
+	}
+
+	if r.defaultRuntime == PackageRuntimeUnspecified {
+		r.defaultRuntime = PackageRuntimeDeployment
+	}
+
+	return r
+}
+
+// For returns the associated runtime for a given kind.
+func (r ActiveRuntime) For(kind string) PackageRuntime {
+	if runtime, ok := r.runtimes[PackageKind(kind)]; ok {
+		return runtime
+	}
+
+	return r.defaultRuntime
+}
+
 // PackageRuntime is the runtime to use for packages with runtime.
 type PackageRuntime string
 
+// PackageKind is the name of the package kind.
+type PackageKind string
+
 const (
 	// PackageRuntimeUnspecified means no package runtime is specified.
 	PackageRuntimeUnspecified PackageRuntime = ""
-
 	// PackageRuntimeDeployment uses a Kubernetes Deployment as the package
 	// runtime.
 	PackageRuntimeDeployment PackageRuntime = "Deployment"
-
 	// PackageRuntimeExternal defer package runtime to an external controller.
 	PackageRuntimeExternal PackageRuntime = "External"
+
+	// ConfigurationPackageKind is for [Configuration,ConfigurationRevision].pkg.crossplane.io.
+	ConfigurationPackageKind PackageKind = "Configuration"
+	// ProviderPackageKind is for [Provider,ProviderRevision].pkg.crossplane.io.
+	ProviderPackageKind PackageKind = "Provider"
+	// FunctionPackageKind is for [Function,FunctionRevision].pkg.crossplane.io.
+	FunctionPackageKind PackageKind = "Function"
 )
+
+// ParsePackageRuntime takes in a free form package runtime option string and attempts to parse it into an ActiveRuntime.
+// Example input: 'Provider=Deployment;Function=External'.
+// Note, This input matches the kong cli parsing for a map type input.
+func ParsePackageRuntime(input string) (ActiveRuntime, error) {
+	var opts []RuntimeOption
+
+	for _, pkgRt := range strings.Split(input, ";") {
+		parts := strings.Split(pkgRt, "=")
+		if len(parts) != 2 {
+			return ActiveRuntime{}, errors.Errorf("invalid package runtime setting %q, expected: runtime=kind", pkgRt)
+		}
+
+		pkg := PackageKind(strings.TrimSpace(parts[0]))
+		rt := PackageRuntime(strings.TrimSpace(parts[1]))
+
+		switch pkg {
+		case ConfigurationPackageKind, ProviderPackageKind, FunctionPackageKind:
+			switch rt {
+			case PackageRuntimeDeployment, PackageRuntimeExternal:
+				opts = append(opts, WithPackageRuntime(pkg, rt))
+			case PackageRuntimeUnspecified:
+				fallthrough
+			default:
+				return ActiveRuntime{}, errors.Errorf("unknown package runtime %q, supported: [%s]", rt,
+					strings.Join([]string{string(PackageRuntimeDeployment), string(PackageRuntimeExternal)}, ", "))
+			}
+		default:
+			return ActiveRuntime{}, errors.Errorf("unknown package runtime kind %q, supported: [%s]", pkg,
+				strings.Join([]string{string(ConfigurationPackageKind), string(ProviderPackageKind), string(FunctionPackageKind)}, ", "))
+		}
+	}
+
+	return NewActiveRuntime(opts...), nil
+}
diff --git a/internal/controller/pkg/manager/reconciler.go b/internal/controller/pkg/manager/reconciler.go
index fb05760ab..4b3bc0c3f 100644
--- a/internal/controller/pkg/manager/reconciler.go
+++ b/internal/controller/pkg/manager/reconciler.go
@@ -149,6 +149,23 @@ func WithRecorder(er event.Recorder) ReconcilerOption {
 	}
 }
 
+// WithManagingRevisionRuntimeSpec will allow this reconciler to propagate the
+// runtime spec fields to revisions.
+func WithManagingRevisionRuntimeSpec() ReconcilerOption {
+	return func(r *Reconciler) {
+		r.setPackageRuntimeManagedFields = func(p v1.Package, pr v1.PackageRevision) {
+			pwr, pwok := p.(v1.PackageWithRuntime)
+
+			prwr, prok := pr.(v1.PackageRevisionWithRuntime)
+			if pwok && prok {
+				prwr.SetRuntimeConfigRef(pwr.GetRuntimeConfigRef())
+				prwr.SetTLSServerSecretName(pwr.GetTLSServerSecretName())
+				prwr.SetTLSClientSecretName(pwr.GetTLSClientSecretName())
+			}
+		}
+	}
+}
+
 // Reconciler reconciles packages.
 type Reconciler struct {
 	client     resource.ClientApplicator
@@ -158,6 +175,8 @@ type Reconciler struct {
 	record     event.Recorder
 	conditions conditions.Manager
 
+	setPackageRuntimeManagedFields func(p v1.Package, pr v1.PackageRevision)
+
 	newPackage             func() v1.Package
 	newPackageRevision     func() v1.PackageRevision
 	newPackageRevisionList func() v1.PackageRevisionList
@@ -191,6 +210,10 @@ func SetupProvider(mgr ctrl.Manager, o controller.Options) error {
 		WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
 	}
 
+	if o.PackageRuntime.For(v1.ProviderKind) == controller.PackageRuntimeDeployment {
+		opts = append(opts, WithManagingRevisionRuntimeSpec())
+	}
+
 	return ctrl.NewControllerManagedBy(mgr).
 		Named(name).
 		For(&v1.Provider{}).
@@ -265,6 +288,10 @@ func SetupFunction(mgr ctrl.Manager, o controller.Options) error {
 		WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
 	}
 
+	if o.PackageRuntime.For(v1.FunctionKind) == controller.PackageRuntimeDeployment {
+		opts = append(opts, WithManagingRevisionRuntimeSpec())
+	}
+
 	return ctrl.NewControllerManagedBy(mgr).
 		Named(name).
 		For(&v1.Function{}).
@@ -517,13 +544,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 	pr.SetSkipDependencyResolution(p.GetSkipDependencyResolution())
 	pr.SetCommonLabels(p.GetCommonLabels())
 
-	pwr, pwok := p.(v1.PackageWithRuntime)
-
-	prwr, prok := pr.(v1.PackageRevisionWithRuntime)
-	if pwok && prok {
-		prwr.SetRuntimeConfigRef(pwr.GetRuntimeConfigRef())
-		prwr.SetTLSServerSecretName(pwr.GetTLSServerSecretName())
-		prwr.SetTLSClientSecretName(pwr.GetTLSClientSecretName())
+	if r.setPackageRuntimeManagedFields != nil {
+		r.setPackageRuntimeManagedFields(p, pr)
 	}
 
 	// If the current revision is not active, and we have an automatic or
diff --git a/internal/controller/pkg/pkg.go b/internal/controller/pkg/pkg.go
index 464b7c434..2c45df37a 100644
--- a/internal/controller/pkg/pkg.go
+++ b/internal/controller/pkg/pkg.go
@@ -20,6 +20,7 @@ package pkg
 import (
 	ctrl "sigs.k8s.io/controller-runtime"
 
+	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/internal/controller/pkg/controller"
 	"github.com/crossplane/crossplane/internal/controller/pkg/manager"
 	"github.com/crossplane/crossplane/internal/controller/pkg/resolver"
@@ -41,9 +42,14 @@ func Setup(mgr ctrl.Manager, o controller.Options) error {
 		revision.SetupFunctionRevision,
 	}
 
-	if o.PackageRuntime == controller.PackageRuntimeDeployment {
+	if o.PackageRuntime.For(pkgv1.ProviderKind) == controller.PackageRuntimeDeployment {
 		setupFuncs = append(setupFuncs, []func(c ctrl.Manager, options controller.Options) error{
 			runtime.SetupProviderRevision,
+		}...)
+	}
+
+	if o.PackageRuntime.For(pkgv1.FunctionKind) == controller.PackageRuntimeDeployment {
+		setupFuncs = append(setupFuncs, []func(c ctrl.Manager, options controller.Options) error{
 			runtime.SetupFunctionRevision,
 		}...)
 	}
diff --git a/internal/controller/pkg/revision/establisher.go b/internal/controller/pkg/revision/establisher.go
index 97cce6303..dbf54cd53 100644
--- a/internal/controller/pkg/revision/establisher.go
+++ b/internal/controller/pkg/revision/establisher.go
@@ -401,7 +401,7 @@ func (e *APIEstablisher) enrichControlledResource(res runtime.Object, webhookTLS
 // getWebhookTLSCert returns the TLS certificate of the webhook server if the
 // revision has a TLS server secret name.
 func (e *APIEstablisher) getWebhookTLSCert(ctx context.Context, parentWithRuntime v1.PackageRevisionWithRuntime) (webhookTLSCert []byte, err error) {
-	tlsServerSecretName := parentWithRuntime.GetTLSServerSecretName()
+	tlsServerSecretName := parentWithRuntime.GetObservedTLSServerSecretName()
 	if tlsServerSecretName == nil {
 		return nil, nil
 	}
diff --git a/internal/controller/pkg/runtime/reconciler.go b/internal/controller/pkg/runtime/reconciler.go
index e3018b76c..2b9c70c77 100644
--- a/internal/controller/pkg/runtime/reconciler.go
+++ b/internal/controller/pkg/runtime/reconciler.go
@@ -61,9 +61,10 @@ const (
 	errPreHook                = "pre establish runtime hook failed for package"
 	errPostHook               = "post establish runtime hook failed for package"
 
-	errNoRuntimeConfig   = "no deployment runtime config set"
-	errGetRuntimeConfig  = "cannot get referenced deployment runtime config"
-	errGetServiceAccount = "cannot get Crossplane service account"
+	errNoRuntimeConfig          = "no deployment runtime config set"
+	errGetRuntimeConfig         = "cannot get referenced deployment runtime config"
+	errUnknownKindRuntimeConfig = "runtime config is set but is an unknown apiVersion and kind"
+	errGetServiceAccount        = "cannot get Crossplane service account"
 )
 
 // Event reasons.
@@ -412,6 +413,11 @@ func (r *Reconciler) builderOptions(ctx context.Context, pwr v1.PackageRevisionW
 			return nil, errors.New(errNoRuntimeConfig)
 		}
 
+		if rcRef.Kind != nil && rcRef.APIVersion != nil &&
+			(*rcRef.Kind != v1beta1.DeploymentRuntimeConfigKind && *rcRef.APIVersion != v1beta1.SchemeGroupVersion.String()) {
+			return nil, errors.New(errUnknownKindRuntimeConfig)
+		}
+
 		rc := &v1beta1.DeploymentRuntimeConfig{}
 		if err := r.client.Get(ctx, types.NamespacedName{Name: rcRef.Name}, rc); err != nil {
 			return nil, errors.Wrap(err, errGetRuntimeConfig)
diff --git a/internal/controller/pkg/runtime/runtime.go b/internal/controller/pkg/runtime/runtime.go
index ce14af15e..8fa1e49ef 100644
--- a/internal/controller/pkg/runtime/runtime.go
+++ b/internal/controller/pkg/runtime/runtime.go
@@ -250,12 +250,12 @@ func (b *DeploymentRuntimeBuilder) Deployment(serviceAccount string, overrides .
 		allOverrides = append(allOverrides, DeploymentRuntimeWithImagePullPolicy(*b.revision.GetPackagePullPolicy()))
 	}
 
-	if b.revision.GetTLSClientSecretName() != nil {
-		allOverrides = append(allOverrides, DeploymentRuntimeWithTLSClientSecret(*b.revision.GetTLSClientSecretName()))
+	if b.revision.GetObservedTLSClientSecretName() != nil {
+		allOverrides = append(allOverrides, DeploymentRuntimeWithTLSClientSecret(*b.revision.GetObservedTLSClientSecretName()))
 	}
 
-	if b.revision.GetTLSServerSecretName() != nil {
-		allOverrides = append(allOverrides, DeploymentRuntimeWithTLSServerSecret(*b.revision.GetTLSServerSecretName()))
+	if b.revision.GetObservedTLSServerSecretName() != nil {
+		allOverrides = append(allOverrides, DeploymentRuntimeWithTLSServerSecret(*b.revision.GetObservedTLSServerSecretName()))
 	}
 
 	// We append the overrides passed to the function last so that they can
@@ -301,13 +301,13 @@ func (b *DeploymentRuntimeBuilder) Service(overrides ...ServiceOverride) *corev1
 
 // TLSClientSecret builds and returns the Secret manifest for the TLS client certificate.
 func (b *DeploymentRuntimeBuilder) TLSClientSecret() *corev1.Secret {
-	if b.revision.GetTLSClientSecretName() == nil {
+	if b.revision.GetObservedTLSClientSecretName() == nil {
 		return nil
 	}
 
 	return &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:            *b.revision.GetTLSClientSecretName(),
+			Name:            *b.revision.GetObservedTLSClientSecretName(),
 			Namespace:       b.namespace,
 			OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(b.revision, b.revision.GetObjectKind().GroupVersionKind()))},
 		},
@@ -316,13 +316,13 @@ func (b *DeploymentRuntimeBuilder) TLSClientSecret() *corev1.Secret {
 
 // TLSServerSecret builds and returns the Secret manifest for the TLS server certificate.
 func (b *DeploymentRuntimeBuilder) TLSServerSecret() *corev1.Secret {
-	if b.revision.GetTLSServerSecretName() == nil {
+	if b.revision.GetObservedTLSServerSecretName() == nil {
 		return nil
 	}
 
 	return &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:            *b.revision.GetTLSServerSecretName(),
+			Name:            *b.revision.GetObservedTLSServerSecretName(),
 			Namespace:       b.namespace,
 			OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(b.revision, b.revision.GetObjectKind().GroupVersionKind()))},
 		},
diff --git a/internal/controller/pkg/runtime/runtime_function.go b/internal/controller/pkg/runtime/runtime_function.go
index 81ae67352..87298820e 100644
--- a/internal/controller/pkg/runtime/runtime_function.go
+++ b/internal/controller/pkg/runtime/runtime_function.go
@@ -67,6 +67,9 @@ func (h *FunctionHooks) Pre(ctx context.Context, pr v1.PackageRevisionWithRuntim
 		return nil
 	}
 
+	pr.SetObservedTLSServerSecretName(pr.GetTLSServerSecretName())
+	pr.SetObservedTLSClientSecretName(pr.GetTLSClientSecretName())
+
 	// Ensure Prerequisites
 	// Note(turkenh): We need certificates have generated when we get to the
 	// establish step, i.e., we want to inject the CA to CRDs (webhook caBundle).
diff --git a/internal/controller/pkg/runtime/runtime_provider.go b/internal/controller/pkg/runtime/runtime_provider.go
index 27c21d316..18ca67202 100644
--- a/internal/controller/pkg/runtime/runtime_provider.go
+++ b/internal/controller/pkg/runtime/runtime_provider.go
@@ -68,6 +68,9 @@ func (h *ProviderHooks) Pre(ctx context.Context, pr v1.PackageRevisionWithRuntim
 		return nil
 	}
 
+	pr.SetObservedTLSServerSecretName(pr.GetTLSServerSecretName())
+	pr.SetObservedTLSClientSecretName(pr.GetTLSClientSecretName())
+
 	// Ensure Prerequisites
 	// Note(turkenh): We need certificates have generated when we get to the
 	// establish step, i.e., we want to inject the CA to CRDs (webhook caBundle).
@@ -212,7 +215,7 @@ func providerDeploymentOverrides(pr v1.PackageRevisionWithRuntime, image string)
 
 	do = append(do, DeploymentRuntimeWithOptionalImage(image))
 
-	if pr.GetTLSClientSecretName() != nil {
+	if pr.GetObservedTLSClientSecretName() != nil {
 		do = append(do, DeploymentRuntimeWithAdditionalEnvironments([]corev1.EnvVar{
 			// for backward compatibility with existing providers, we set the
 			// environment variable ESS_TLS_CERTS_DIR to the same value as
@@ -224,7 +227,7 @@ func providerDeploymentOverrides(pr v1.PackageRevisionWithRuntime, image string)
 		}))
 	}
 
-	if pr.GetTLSServerSecretName() != nil {
+	if pr.GetObservedTLSServerSecretName() != nil {
 		do = append(do, DeploymentRuntimeWithAdditionalPorts([]corev1.ContainerPort{
 			{
 				Name:          WebhookPortName,



Test patch:
diff --git a/apis/pkg/v1/conditions_test.go b/apis/pkg/v1/conditions_test.go
index a9b6185a6..ce21c3cf3 100644
--- a/apis/pkg/v1/conditions_test.go
+++ b/apis/pkg/v1/conditions_test.go
@@ -27,20 +27,22 @@ func TestPackageHealth(t *testing.T) {
 			reason: "Should return healthy condition when both revision and runtime are healthy for package with runtime",
 			args: args{
 				pr: &ProviderRevision{
-					Status: PackageRevisionStatus{
-						ConditionedStatus: commonv1.ConditionedStatus{
-							Conditions: []commonv1.Condition{
-								{
-									Type:    TypeRevisionHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package revision is healthy",
-								},
-								{
-									Type:    TypeRuntimeHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package runtime is healthy",
+					Status: ProviderRevisionStatus{
+						PackageRevisionStatus: PackageRevisionStatus{
+							ConditionedStatus: commonv1.ConditionedStatus{
+								Conditions: []commonv1.Condition{
+									{
+										Type:    TypeRevisionHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package revision is healthy",
+									},
+									{
+										Type:    TypeRuntimeHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package runtime is healthy",
+									},
 								},
 							},
 						},
@@ -77,20 +79,22 @@ func TestPackageHealth(t *testing.T) {
 			reason: "Should return unhealthy condition when revision is unhealthy",
 			args: args{
 				pr: &ProviderRevision{
-					Status: PackageRevisionStatus{
-						ConditionedStatus: commonv1.ConditionedStatus{
-							Conditions: []commonv1.Condition{
-								{
-									Type:    TypeRevisionHealthy,
-									Status:  corev1.ConditionFalse,
-									Reason:  ReasonUnhealthy,
-									Message: "Package revision is not ready",
-								},
-								{
-									Type:    TypeRuntimeHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package runtime is healthy",
+					Status: ProviderRevisionStatus{
+						PackageRevisionStatus: PackageRevisionStatus{
+							ConditionedStatus: commonv1.ConditionedStatus{
+								Conditions: []commonv1.Condition{
+									{
+										Type:    TypeRevisionHealthy,
+										Status:  corev1.ConditionFalse,
+										Reason:  ReasonUnhealthy,
+										Message: "Package revision is not ready",
+									},
+									{
+										Type:    TypeRuntimeHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package runtime is healthy",
+									},
 								},
 							},
 						},
@@ -105,20 +109,22 @@ func TestPackageHealth(t *testing.T) {
 			reason: "Should return unhealthy condition when runtime is unhealthy",
 			args: args{
 				pr: &ProviderRevision{
-					Status: PackageRevisionStatus{
-						ConditionedStatus: commonv1.ConditionedStatus{
-							Conditions: []commonv1.Condition{
-								{
-									Type:    TypeRevisionHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package revision is healthy",
-								},
-								{
-									Type:    TypeRuntimeHealthy,
-									Status:  corev1.ConditionFalse,
-									Reason:  ReasonUnhealthy,
-									Message: "Runtime deployment is not ready",
+					Status: ProviderRevisionStatus{
+						PackageRevisionStatus: PackageRevisionStatus{
+							ConditionedStatus: commonv1.ConditionedStatus{
+								Conditions: []commonv1.Condition{
+									{
+										Type:    TypeRevisionHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package revision is healthy",
+									},
+									{
+										Type:    TypeRuntimeHealthy,
+										Status:  corev1.ConditionFalse,
+										Reason:  ReasonUnhealthy,
+										Message: "Runtime deployment is not ready",
+									},
 								},
 							},
 						},
@@ -163,19 +169,21 @@ func TestPackageHealth(t *testing.T) {
 			reason: "Should return unhealthy condition when revision health is unknown",
 			args: args{
 				pr: &ProviderRevision{
-					Status: PackageRevisionStatus{
-						ConditionedStatus: commonv1.ConditionedStatus{
-							Conditions: []commonv1.Condition{
-								{
-									Type:   TypeRevisionHealthy,
-									Status: corev1.ConditionUnknown,
-									Reason: ReasonUnknownHealth,
-								},
-								{
-									Type:    TypeRuntimeHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package runtime is healthy",
+					Status: ProviderRevisionStatus{
+						PackageRevisionStatus: PackageRevisionStatus{
+							ConditionedStatus: commonv1.ConditionedStatus{
+								Conditions: []commonv1.Condition{
+									{
+										Type:   TypeRevisionHealthy,
+										Status: corev1.ConditionUnknown,
+										Reason: ReasonUnknownHealth,
+									},
+									{
+										Type:    TypeRuntimeHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package runtime is healthy",
+									},
 								},
 							},
 						},
@@ -190,19 +198,21 @@ func TestPackageHealth(t *testing.T) {
 			reason: "Should return unhealthy condition when runtime health is unknown for package with runtime",
 			args: args{
 				pr: &ProviderRevision{
-					Status: PackageRevisionStatus{
-						ConditionedStatus: commonv1.ConditionedStatus{
-							Conditions: []commonv1.Condition{
-								{
-									Type:    TypeRevisionHealthy,
-									Status:  corev1.ConditionTrue,
-									Reason:  ReasonHealthy,
-									Message: "Package revision is healthy",
-								},
-								{
-									Type:   TypeRuntimeHealthy,
-									Status: corev1.ConditionUnknown,
-									Reason: ReasonUnknownHealth,
+					Status: ProviderRevisionStatus{
+						PackageRevisionStatus: PackageRevisionStatus{
+							ConditionedStatus: commonv1.ConditionedStatus{
+								Conditions: []commonv1.Condition{
+									{
+										Type:    TypeRevisionHealthy,
+										Status:  corev1.ConditionTrue,
+										Reason:  ReasonHealthy,
+										Message: "Package revision is healthy",
+									},
+									{
+										Type:   TypeRuntimeHealthy,
+										Status: corev1.ConditionUnknown,
+										Reason: ReasonUnknownHealth,
+									},
 								},
 							},
 						},
diff --git a/internal/controller/apiextensions/composite/composition_functions_test.go b/internal/controller/apiextensions/composite/composition_functions_test.go
index 8911b8186..f84ede600 100644
--- a/internal/controller/apiextensions/composite/composition_functions_test.go
+++ b/internal/controller/apiextensions/composite/composition_functions_test.go
@@ -558,6 +558,193 @@ func TestFunctionCompose(t *testing.T) {
 				err: errors.Wrap(errBoom, errGarbageCollectCDs),
 			},
 		},
+		"NamespacedXRClusterCompositionError": {
+			reason: "We should return an error when a namespaced XR tries to compose cluster-scoped resources",
+			params: params{
+				c: &test.MockClient{
+					MockPatch:              test.NewMockPatchFn(nil),
+					MockStatusPatch:        test.NewMockSubResourcePatchFn(nil),
+					MockIsObjectNamespaced: test.NewMockIsObjectNamespacedFn(nil, false),
+				},
+				uc: &test.MockClient{
+					MockGet: test.NewMockGetFn(errBoom),
+				},
+				r: FunctionRunnerFn(func(_ context.Context, _ string, _ *fnv1.RunFunctionRequest) (rsp *fnv1.RunFunctionResponse, err error) {
+					d := &fnv1.State{
+						Resources: map[string]*fnv1.Resource{
+							"cluster-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "ClusterComposed",
+									"metadata": map[string]any{
+										"name": "cluster-resource",
+									},
+								}),
+							},
+						},
+					}
+					return &fnv1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(_ context.Context, _ ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(_ context.Context, _ resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(_ context.Context, _ metav1.Object, _, _ ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: func() *composite.Unstructured {
+					xr := WithParentLabel()
+					xr.SetNamespace("test-namespace") // Make the XR namespaced
+					return xr
+				}(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Errorf(errFmtNamespacedXRClusterResource, "cluster-resource", "ClusterComposed", "cluster-resource"),
+			},
+		},
+		"NamespacedXRNamespacedComposition": {
+			reason: "We should succeed when a namespaced XR tries to compose namespaced-scoped resources",
+			params: params{
+				c: &test.MockClient{
+					MockPatch:              test.NewMockPatchFn(nil),
+					MockStatusPatch:        test.NewMockSubResourcePatchFn(nil),
+					MockIsObjectNamespaced: test.NewMockIsObjectNamespacedFn(nil, true),
+				},
+				uc: &test.MockClient{
+					MockGet: test.NewMockGetFn(errBoom),
+				},
+				r: FunctionRunnerFn(func(_ context.Context, _ string, _ *fnv1.RunFunctionRequest) (rsp *fnv1.RunFunctionResponse, err error) {
+					d := &fnv1.State{
+						Resources: map[string]*fnv1.Resource{
+							"ns-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "NamespaceComposed",
+									"metadata": map[string]any{
+										"name": "ns-resource",
+									},
+								}),
+							},
+						},
+					}
+					return &fnv1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(_ context.Context, _ ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(_ context.Context, _ resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(_ context.Context, _ metav1.Object, _, _ ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: func() *composite.Unstructured {
+					xr := WithParentLabel()
+					xr.SetNamespace("test-namespace") // Make the XR namespaced
+					return xr
+				}(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				res: CompositionResult{
+					Composed: []ComposedResource{{ResourceName: "ns-resource", Ready: false, Synced: true}},
+				},
+			},
+		},
+		"ClusterXRClusterCompositionSuccess": {
+			reason: "Cluster-scoped XRs should be allowed to compose cluster-scoped resources",
+			params: params{
+				c: &test.MockClient{
+					MockGet:                test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{Resource: "ClusterComposed"}, "")), // all names are available
+					MockPatch:              test.NewMockPatchFn(nil),
+					MockStatusPatch:        test.NewMockSubResourcePatchFn(nil),
+					MockIsObjectNamespaced: test.NewMockIsObjectNamespacedFn(errBoom, false),
+				},
+				uc: &test.MockClient{
+					MockGet: test.NewMockGetFn(errBoom),
+				},
+				r: FunctionRunnerFn(func(_ context.Context, _ string, _ *fnv1.RunFunctionRequest) (rsp *fnv1.RunFunctionResponse, err error) {
+					d := &fnv1.State{
+						Resources: map[string]*fnv1.Resource{
+							"cluster-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "ClusterComposed",
+									"metadata": map[string]any{
+										"name": "cluster-resource",
+									},
+								}),
+							},
+						},
+					}
+					return &fnv1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(_ context.Context, _ ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(_ context.Context, _ resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(_ context.Context, _ metav1.Object, _, _ ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: WithParentLabel(), // Cluster-scoped XR (no namespace)
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				res: CompositionResult{
+					Composed: []ComposedResource{{ResourceName: "cluster-resource", Ready: false, Synced: true}},
+				},
+			},
+		},
 		"ApplyXRResourceReferencesError": {
 			reason: "We should return any error we encounter when applying the composite resource's resource references",
 			params: params{
diff --git a/internal/controller/pkg/controller/runtime_test.go b/internal/controller/pkg/controller/runtime_test.go
new file mode 100644
index 000000000..bb8792026
--- /dev/null
+++ b/internal/controller/pkg/controller/runtime_test.go
@@ -0,0 +1,208 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+func TestParsePackageRuntime(t *testing.T) {
+	type args struct {
+		input string
+	}
+
+	type want struct {
+		runtime ActiveRuntime
+		err     error
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"EmptyInput": {
+			reason: "Should return default runtime when input is empty",
+			args: args{
+				input: "",
+			},
+			want: want{
+				err: errors.New("invalid package runtime setting \"\", expected: runtime=kind"),
+			},
+		},
+		"SingleValidMapping": {
+			reason: "Should parse single valid package runtime mapping",
+			args: args{
+				input: "Provider=Deployment",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeDeployment),
+				),
+			},
+		},
+		"MultipleValidMappings": {
+			reason: "Should parse multiple valid package runtime mappings",
+			args: args{
+				input: "Provider=Deployment;Function=External",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeDeployment),
+					WithPackageRuntime(FunctionPackageKind, PackageRuntimeExternal),
+				),
+			},
+		},
+		"AllValidMappings": {
+			reason: "Should parse all valid package kinds and runtimes",
+			args: args{
+				input: "Provider=Deployment;Function=External;Configuration=Deployment",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeDeployment),
+					WithPackageRuntime(FunctionPackageKind, PackageRuntimeExternal),
+					WithPackageRuntime(ConfigurationPackageKind, PackageRuntimeDeployment),
+				),
+			},
+		},
+		"InvalidFormat": {
+			reason: "Should return error for invalid format",
+			args: args{
+				input: "Provider",
+			},
+			want: want{
+				err: errors.New("invalid package runtime setting \"Provider\", expected: runtime=kind"),
+			},
+		},
+		"InvalidFormatMultipleEquals": {
+			reason: "Should return error for multiple equals signs",
+			args: args{
+				input: "Provider=Deployment=Extra",
+			},
+			want: want{
+				err: errors.New("invalid package runtime setting \"Provider=Deployment=Extra\", expected: runtime=kind"),
+			},
+		},
+		"InvalidFormatNoEquals": {
+			reason: "Should return error for no equals sign",
+			args: args{
+				input: "ProviderDeployment",
+			},
+			want: want{
+				err: errors.New("invalid package runtime setting \"ProviderDeployment\", expected: runtime=kind"),
+			},
+		},
+		"UnknownPackageKind": {
+			reason: "Should return error for unknown package kind",
+			args: args{
+				input: "UnknownKind=Deployment",
+			},
+			want: want{
+				err: errors.New("unknown package runtime kind \"UnknownKind\", supported: [Configuration, Provider, Function]"),
+			},
+		},
+		"UnknownRuntime": {
+			reason: "Should return error for unknown runtime",
+			args: args{
+				input: "Provider=UnknownRuntime",
+			},
+			want: want{
+				err: errors.New("unknown package runtime \"UnknownRuntime\", supported: [Deployment, External]"),
+			},
+		},
+		"MixedValidAndInvalid": {
+			reason: "Should return error on first invalid mapping even if others are valid",
+			args: args{
+				input: "Provider=Deployment;UnknownKind=External",
+			},
+			want: want{
+				err: errors.New("unknown package runtime kind \"UnknownKind\", supported: [Configuration, Provider, Function]"),
+			},
+		},
+		"EmptyPackageKind": {
+			reason: "Should return error for empty package kind",
+			args: args{
+				input: "=Deployment",
+			},
+			want: want{
+				err: errors.New("unknown package runtime kind \"\", supported: [Configuration, Provider, Function]"),
+			},
+		},
+		"EmptyRuntime": {
+			reason: "Should return error for empty runtime",
+			args: args{
+				input: "Provider=",
+			},
+			want: want{
+				err: errors.New("unknown package runtime \"\", supported: [Deployment, External]"),
+			},
+		},
+		"WhitespaceInInput": {
+			reason: "Should handle whitespace in input",
+			args: args{
+				input: " Provider = Deployment ",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeDeployment),
+				),
+			},
+		},
+		"ValidExternalRuntime": {
+			reason: "Should parse valid External runtime",
+			args: args{
+				input: "Function=External",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(FunctionPackageKind, PackageRuntimeExternal),
+				),
+			},
+		},
+		"DuplicatePackageKind": {
+			reason: "Should handle duplicate package kinds (last one wins)",
+			args: args{
+				input: "Provider=Deployment;Provider=External",
+			},
+			want: want{
+				runtime: NewActiveRuntime(
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeDeployment),
+					WithPackageRuntime(ProviderPackageKind, PackageRuntimeExternal),
+				),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got, err := ParsePackageRuntime(tc.args.input)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nParsePackageRuntime(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+
+			if diff := cmp.Diff(tc.want.runtime, got, cmp.AllowUnexported(ActiveRuntime{})); diff != "" {
+				t.Errorf("\n%s\nParsePackageRuntime(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
diff --git a/internal/controller/pkg/revision/establisher_test.go b/internal/controller/pkg/revision/establisher_test.go
index ce704fa41..220a9e34d 100644
--- a/internal/controller/pkg/revision/establisher_test.go
+++ b/internal/controller/pkg/revision/establisher_test.go
@@ -198,8 +198,8 @@ func TestAPIEstablisherEstablish(t *testing.T) {
 							v1.LabelParentPackage: "provider-name",
 						},
 					},
-					Spec: v1.ProviderRevisionSpec{
-						PackageRevisionRuntimeSpec: v1.PackageRevisionRuntimeSpec{
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
 							TLSServerSecretName: &tlsServerSecretName,
 						},
 					},
@@ -301,8 +301,8 @@ func TestAPIEstablisherEstablish(t *testing.T) {
 					MockGet: test.NewMockGetFn(errBoom),
 				}),
 				parent: &v1.ProviderRevision{
-					Spec: v1.ProviderRevisionSpec{
-						PackageRevisionRuntimeSpec: v1.PackageRevisionRuntimeSpec{
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
 							TLSServerSecretName: &tlsServerSecretName,
 						},
 					},
@@ -343,8 +343,8 @@ func TestAPIEstablisherEstablish(t *testing.T) {
 					},
 				}),
 				parent: &v1.ProviderRevision{
-					Spec: v1.ProviderRevisionSpec{
-						PackageRevisionRuntimeSpec: v1.PackageRevisionRuntimeSpec{
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
 							TLSServerSecretName: &tlsServerSecretName,
 						},
 					},
@@ -478,12 +478,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
@@ -505,12 +507,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
@@ -552,12 +556,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
@@ -621,12 +627,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
@@ -680,12 +688,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
@@ -736,12 +746,14 @@ func TestAPIEstablisherReleaseObjects(t *testing.T) {
 					ObjectMeta: metav1.ObjectMeta{
 						UID: "some-unique-uid-2312",
 					},
-					Status: v1.PackageRevisionStatus{
-						ObjectRefs: []xpv1.TypedReference{
-							{
-								APIVersion: "apiextensions.k8s.io/v1",
-								Kind:       "CustomResourceDefinition",
-								Name:       "releases.helm.crossplane.io",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ObjectRefs: []xpv1.TypedReference{
+								{
+									APIVersion: "apiextensions.k8s.io/v1",
+									Kind:       "CustomResourceDefinition",
+									Name:       "releases.helm.crossplane.io",
+								},
 							},
 						},
 					},
diff --git a/internal/controller/pkg/revision/imageback_test.go b/internal/controller/pkg/revision/imageback_test.go
index 923919e5f..6f69904eb 100644
--- a/internal/controller/pkg/revision/imageback_test.go
+++ b/internal/controller/pkg/revision/imageback_test.go
@@ -91,8 +91,10 @@ func TestImageBackend(t *testing.T) {
 							Package: ":test",
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: ":test",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: ":test",
+						},
 					},
 				})},
 			},
@@ -110,8 +112,10 @@ func TestImageBackend(t *testing.T) {
 							Package: "xpkg.crossplane.io/test/test:latest",
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+						},
 					},
 				})},
 			},
@@ -129,8 +133,10 @@ func TestImageBackend(t *testing.T) {
 							Package: "xpkg.crossplane.io/test/test:latest",
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+						},
 					},
 				})},
 			},
@@ -148,8 +154,10 @@ func TestImageBackend(t *testing.T) {
 							Package: "xpkg.crossplane.io/test/test:latest",
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+						},
 					},
 				})},
 			},
@@ -167,8 +175,10 @@ func TestImageBackend(t *testing.T) {
 							Package: "xpkg.crossplane.io/test/test:latest",
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: "xpkg.crossplane.io/test/test:latest",
+						},
 					},
 				})},
 			},
@@ -183,7 +193,7 @@ func TestImageBackend(t *testing.T) {
 		// 		},
 		// 		opts: []parser.BackendOption{PackageRevision(&v1.ProviderRevision{
 		// 			Spec: v1.PackageRevisionSpec{
-		// 				Package: "test/test:latest",
+		// 				Package: "xpkg.crossplane.io/test/test:latest",
 		// 			},
 		// 		})},
 		// 	},
diff --git a/internal/controller/pkg/revision/reconciler_test.go b/internal/controller/pkg/revision/reconciler_test.go
index 661b63202..61e184c78 100644
--- a/internal/controller/pkg/revision/reconciler_test.go
+++ b/internal/controller/pkg/revision/reconciler_test.go
@@ -1380,12 +1380,14 @@ func TestReconcile(t *testing.T) {
 				rec: []ReconcilerOption{
 					WithNewPackageRevisionFn(func() v1.PackageRevision {
 						return &v1.ProviderRevision{
-							Status: v1.PackageRevisionStatus{
-								ObjectRefs: []xpv1.TypedReference{
-									{
-										APIVersion: "apiextensions.k8s.io/v1",
-										Kind:       "CustomResourceDefinition",
-										Name:       "releases.helm.crossplane.io",
+							Status: v1.ProviderRevisionStatus{
+								PackageRevisionStatus: v1.PackageRevisionStatus{
+									ObjectRefs: []xpv1.TypedReference{
+										{
+											APIVersion: "apiextensions.k8s.io/v1",
+											Kind:       "CustomResourceDefinition",
+											Name:       "releases.helm.crossplane.io",
+										},
 									},
 								},
 							},
@@ -1404,12 +1406,14 @@ func TestReconcile(t *testing.T) {
 							}),
 							MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(o client.Object) error {
 								want := &v1.ProviderRevision{
-									Status: v1.PackageRevisionStatus{
-										ObjectRefs: []xpv1.TypedReference{
-											{
-												APIVersion: "apiextensions.k8s.io/v1",
-												Kind:       "CustomResourceDefinition",
-												Name:       "releases.helm.crossplane.io",
+									Status: v1.ProviderRevisionStatus{
+										PackageRevisionStatus: v1.PackageRevisionStatus{
+											ObjectRefs: []xpv1.TypedReference{
+												{
+													APIVersion: "apiextensions.k8s.io/v1",
+													Kind:       "CustomResourceDefinition",
+													Name:       "releases.helm.crossplane.io",
+												},
 											},
 										},
 									},
@@ -1518,12 +1522,14 @@ func TestReconcile(t *testing.T) {
 				rec: []ReconcilerOption{
 					WithNewPackageRevisionFn(func() v1.PackageRevision {
 						return &v1.ProviderRevision{
-							Status: v1.PackageRevisionStatus{
-								ObjectRefs: []xpv1.TypedReference{
-									{
-										APIVersion: "apiextensions.k8s.io/v1",
-										Kind:       "CustomResourceDefinition",
-										Name:       "releases.helm.crossplane.io",
+							Status: v1.ProviderRevisionStatus{
+								PackageRevisionStatus: v1.PackageRevisionStatus{
+									ObjectRefs: []xpv1.TypedReference{
+										{
+											APIVersion: "apiextensions.k8s.io/v1",
+											Kind:       "CustomResourceDefinition",
+											Name:       "releases.helm.crossplane.io",
+										},
 									},
 								},
 							},
@@ -1542,12 +1548,14 @@ func TestReconcile(t *testing.T) {
 							}),
 							MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(o client.Object) error {
 								want := &v1.ProviderRevision{
-									Status: v1.PackageRevisionStatus{
-										ObjectRefs: []xpv1.TypedReference{
-											{
-												APIVersion: "apiextensions.k8s.io/v1",
-												Kind:       "CustomResourceDefinition",
-												Name:       "releases.helm.crossplane.io",
+									Status: v1.ProviderRevisionStatus{
+										PackageRevisionStatus: v1.PackageRevisionStatus{
+											ObjectRefs: []xpv1.TypedReference{
+												{
+													APIVersion: "apiextensions.k8s.io/v1",
+													Kind:       "CustomResourceDefinition",
+													Name:       "releases.helm.crossplane.io",
+												},
 											},
 										},
 									},
diff --git a/internal/controller/pkg/runtime/runtime_function_test.go b/internal/controller/pkg/runtime/runtime_function_test.go
index fc47d26d1..497b01fd8 100644
--- a/internal/controller/pkg/runtime/runtime_function_test.go
+++ b/internal/controller/pkg/runtime/runtime_function_test.go
@@ -108,6 +108,9 @@ func TestFunctionPreHook(t *testing.T) {
 					},
 					Status: v1.FunctionRevisionStatus{
 						Endpoint: fmt.Sprintf(ServiceEndpointFmt, "some-service", "some-namespace", revision.ServicePort),
+						PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
+							TLSServerSecretName: ptr.To("some-server-secret"),
+						},
 					},
 				},
 			},
diff --git a/internal/controller/pkg/runtime/runtime_provider_test.go b/internal/controller/pkg/runtime/runtime_provider_test.go
index 0f883e127..e113bd174 100644
--- a/internal/controller/pkg/runtime/runtime_provider_test.go
+++ b/internal/controller/pkg/runtime/runtime_provider_test.go
@@ -111,6 +111,12 @@ func TestProviderPreHook(t *testing.T) {
 							TLSServerSecretName: ptr.To("some-server-secret"),
 						},
 					},
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
+							TLSClientSecretName: ptr.To("some-client-secret"),
+							TLSServerSecretName: ptr.To("some-server-secret"),
+						},
+					},
 				},
 			},
 		},
@@ -183,8 +189,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -212,8 +220,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				err: errors.Wrap(errors.Wrap(errBoom, "cannot patch object"), errApplyProviderSA),
@@ -230,8 +240,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -262,8 +274,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				err: errors.Wrap(errors.Wrap(errBoom, "cannot patch object"), errApplyProviderDeployment),
@@ -280,8 +294,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -309,8 +325,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				err: errors.New(errNoAvailableConditionProviderDeployment),
@@ -327,8 +345,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -364,8 +384,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				err: errors.Errorf(errFmtUnavailableProviderDeployment, errBoom.Error()),
@@ -382,8 +404,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -418,8 +442,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 			},
@@ -435,8 +461,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -474,8 +502,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 			},
@@ -491,8 +521,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 				manifests: &MockManifestBuilder{
@@ -557,8 +589,10 @@ func TestProviderPostHook(t *testing.T) {
 							DesiredState: v1.PackageRevisionActive,
 						},
 					},
-					Status: v1.PackageRevisionStatus{
-						ResolvedPackage: providerImage,
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							ResolvedPackage: providerImage,
+						},
 					},
 				},
 			},
diff --git a/internal/controller/pkg/runtime/runtime_test.go b/internal/controller/pkg/runtime/runtime_test.go
index 06a27e031..9b67c100e 100644
--- a/internal/controller/pkg/runtime/runtime_test.go
+++ b/internal/controller/pkg/runtime/runtime_test.go
@@ -63,7 +63,9 @@ var (
 			PackageRevisionSpec: v1.PackageRevisionSpec{
 				Package: providerImage,
 			},
-			PackageRevisionRuntimeSpec: v1.PackageRevisionRuntimeSpec{
+		},
+		Status: v1.ProviderRevisionStatus{
+			PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
 				TLSServerSecretName: ptr.To(tlsServerSecretName),
 				TLSClientSecretName: ptr.To(tlsClientSecretName),
 			},
@@ -85,7 +87,9 @@ var (
 			PackageRevisionSpec: v1.PackageRevisionSpec{
 				Package: functionImage,
 			},
-			PackageRevisionRuntimeSpec: v1.PackageRevisionRuntimeSpec{
+		},
+		Status: v1.FunctionRevisionStatus{
+			PackageRevisionRuntimeStatus: v1.PackageRevisionRuntimeStatus{
 				TLSServerSecretName: ptr.To(tlsServerSecretName),
 			},
 		},
diff --git a/test/e2e/apiextensions_compositions_test.go b/test/e2e/apiextensions_compositions_test.go
index 093b43d67..ab129f7a7 100644
--- a/test/e2e/apiextensions_compositions_test.go
+++ b/test/e2e/apiextensions_compositions_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package e2e
 
 import (
+	"errors"
 	"testing"
 	"time"
 
@@ -254,3 +255,32 @@ func TestCompositionValidation(t *testing.T) {
 			Feature(),
 	)
 }
+
+func TestNamespacedXRClusterComposition(t *testing.T) {
+	manifests := "test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource"
+	environment.Test(t,
+		features.NewWithDescription(t.Name(), "Tests that namespaced XRs cannot compose cluster-scoped resources, ensuring proper validation and error handling when such invalid compositions are attempted.").
+			WithLabel(LabelArea, LabelAreaAPIExtensions).
+			WithLabel(LabelSize, LabelSizeSmall).
+			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
+			WithSetup("CreatePrerequisites", funcs.AllOf(
+				funcs.ApplyResources(FieldManager, manifests, "setup/*.yaml"),
+				funcs.ResourcesCreatedWithin(30*time.Second, manifests, "setup/*.yaml"),
+				funcs.ResourcesHaveConditionWithin(1*time.Minute, manifests, "setup/definition.yaml", apiextensionsv1.WatchingComposite()),
+				funcs.ResourcesHaveConditionWithin(2*time.Minute, manifests, "setup/functions.yaml", pkgv1.Healthy(), pkgv1.Active()),
+			)).
+			Assess("CreateNamespacedXR", funcs.AllOf(
+				funcs.ApplyResources(FieldManager, manifests, "xr.yaml"),
+				funcs.ResourcesCreatedWithin(30*time.Second, manifests, "xr.yaml"),
+			)).
+			Assess("XRHasReconcileError",
+				funcs.ResourcesHaveConditionWithin(1*time.Minute, manifests, "xr.yaml", xpv1.ReconcileError(errors.New(""))),
+			).
+			WithTeardown("DeleteXR", funcs.AllOf(
+				funcs.DeleteResources(manifests, "xr.yaml"),
+				funcs.ResourcesDeletedWithin(2*time.Minute, manifests, "xr.yaml"),
+			)).
+			WithTeardown("DeletePrerequisites", funcs.ResourcesDeletedAfterListedAreGone(3*time.Minute, manifests, "setup/*.yaml", nopList)).
+			Feature(),
+	)
+}
diff --git a/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/composition.yaml b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/composition.yaml
new file mode 100644
index 000000000..8b5a09676
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/composition.yaml
@@ -0,0 +1,33 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: Composition
+metadata:
+  name: invalid-namespaced-xr-cluster-composition
+spec:
+  compositeTypeRef:
+    apiVersion: validation.example.org/v1alpha1
+    kind: NamespacedTest
+  mode: Pipeline
+  pipeline:
+  - step: create-cluster-resource
+    functionRef:
+      name: function-dummy-ext-basic
+    input:
+      apiVersion: dummy.fn.crossplane.io/v1beta1
+      kind: Response
+      response:
+        desired:
+          resources:
+            cluster-role-test:
+              resource:
+                apiVersion: rbac.authorization.k8s.io/v1
+                kind: ClusterRole
+                metadata:
+                  name: cluster-role
+                rules:
+                  - apiGroups:
+                      - example.org
+                    resources:
+                      - clustertests
+                    verbs:
+                      - '*'
+              ready: READY_TRUE
diff --git a/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/definition.yaml b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/definition.yaml
new file mode 100644
index 000000000..109eaeebf
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/definition.yaml
@@ -0,0 +1,25 @@
+apiVersion: apiextensions.crossplane.io/v2alpha1
+kind: CompositeResourceDefinition
+metadata:
+  name: namespacedtests.validation.example.org
+spec:
+  scope: Namespaced
+  group: validation.example.org
+  names:
+    kind: NamespacedTest
+    plural: namespacedtests
+  versions:
+  - name: v1alpha1
+    served: true
+    referenceable: true
+    schema:
+     openAPIV3Schema:
+       type: object
+       properties:
+        spec:
+          type: object
+          properties:
+            testField:
+              type: string
+          required:
+          - testField
diff --git a/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/functions.yaml b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/functions.yaml
new file mode 100644
index 000000000..22deb63d6
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/setup/functions.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: pkg.crossplane.io/v1beta1
+kind: Function
+metadata:
+  name: function-dummy-ext-basic
+spec:
+  package: xpkg.upbound.io/crossplane-contrib/function-dummy:v0.2.1
diff --git a/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/xr.yaml b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/xr.yaml
new file mode 100644
index 000000000..7924be6b6
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/namespaced-xr-no-cluster-scoped-resource/xr.yaml
@@ -0,0 +1,10 @@
+apiVersion: validation.example.org/v1alpha1
+kind: NamespacedTest
+metadata:
+  name: test-namespaced-xr
+  namespace: crossplane-system
+spec:
+  testField: "test-value"
+  crossplane:
+    compositionRef:
+      name: invalid-namespaced-xr-cluster-composition