diff --git a/PROJECT b/PROJECT index 7abc918..d9c9e93 100644 --- a/PROJECT +++ b/PROJECT @@ -82,4 +82,20 @@ resources: kind: ToolGatewayClass path: github.com/agentic-layer/agent-runtime-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: agentic-layer.ai + group: runtime + kind: Guard + path: github.com/agentic-layer/agent-runtime-operator/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: agentic-layer.ai + group: runtime + kind: GuardrailProvider + path: github.com/agentic-layer/agent-runtime-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/aigateway_types.go b/api/v1alpha1/aigateway_types.go index cc57051..fd2c963 100644 --- a/api/v1alpha1/aigateway_types.go +++ b/api/v1alpha1/aigateway_types.go @@ -48,6 +48,11 @@ type AiGatewaySpec struct { // +optional EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` + // Guardrails lists the Guard resources to be applied to requests through this AI gateway. + // Guards are applied in the order they are listed. + // +optional + Guardrails []corev1.ObjectReference `json:"guardrails,omitempty"` + // CommonMetadata defines labels and annotations to be applied to the Deployment and Service // resources created for this gateway, as well as the pod template. // +optional @@ -59,6 +64,7 @@ type AiGatewaySpec struct { PodMetadata *EmbeddedMetadata `json:"podMetadata,omitempty"` } +// AiModel is an AI model configuration. type AiModel struct { // Name is the identifier for the AI model (e.g., "gpt-4", "claude-3-opus") // +kubebuilder:validation:Required diff --git a/api/v1alpha1/guard_types.go b/api/v1alpha1/guard_types.go new file mode 100644 index 0000000..0c6c17b --- /dev/null +++ b/api/v1alpha1/guard_types.go @@ -0,0 +1,77 @@ +/* +Copyright 2025 Agentic Layer. + +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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GuardSpec defines the desired state of Guard. +type GuardSpec struct { + // Name is the identifier of the guard as known by the referenced GuardrailProvider. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Version is the version of the guard at the provider (if supported). + // +optional + Version string `json:"version,omitempty"` + + // Mode defines when the guard is applied relative to the LLM call. + // +kubebuilder:validation:Enum=pre_call;post_call;during_call + Mode string `json:"mode"` + + // Description provides a human-readable description of the guard's purpose. + // This field is for documentation purposes only and has no effect on the guard's behavior. + // +optional + Description string `json:"description,omitempty"` + + // ProviderRef references the GuardrailProvider that hosts this guard. + // If Namespace is not specified, defaults to the same namespace as the Guard. + ProviderRef corev1.ObjectReference `json:"providerRef"` +} + +// GuardStatus defines the observed state of Guard. +type GuardStatus struct { + // +operator-sdk:csv:customresourcedefinitions:type=status + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Guard is the Schema for the guards API. +type Guard struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GuardSpec `json:"spec,omitempty"` + Status GuardStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// GuardList contains a list of Guard. +type GuardList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Guard `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Guard{}, &GuardList{}) +} diff --git a/api/v1alpha1/guardrailprovider_types.go b/api/v1alpha1/guardrailprovider_types.go new file mode 100644 index 0000000..e4f8fa8 --- /dev/null +++ b/api/v1alpha1/guardrailprovider_types.go @@ -0,0 +1,95 @@ +/* +Copyright 2025 Agentic Layer. + +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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GuardrailProviderSpec defines the desired state of GuardrailProvider. +type GuardrailProviderSpec struct { + // Protocol defines the guardrail protocol used by this provider. + // +kubebuilder:validation:Enum=openai-moderation;bedrock + Protocol string `json:"protocol"` + + // ApiKeySecretRef references a Kubernetes Secret containing the API key for the guardrail provider. + // The secret must contain the key specified in the SecretKeySelector. + // +optional + ApiKeySecretRef *corev1.SecretKeySelector `json:"apiKeySecretRef,omitempty"` + + // TransportType defines the transport used to communicate with the guardrail backend. + // Required when BackendRef is specified. + // +kubebuilder:validation:Enum=http;grpc;envoy-ext-proc + // +optional + TransportType string `json:"transportType,omitempty"` + + // BackendRef references the Kubernetes Service acting as the guardrail backend. + // When omitted, the provider uses the protocol's default managed endpoint + // (e.g., the official OpenAI moderation API or AWS Bedrock). + // Mutually exclusive with ExternalUrl. + // +optional + BackendRef *GuardrailBackendRef `json:"backendRef,omitempty"` + + // ExternalUrl specifies an external URL for the guardrail backend. + // Use this to point to an external guardrail service outside the cluster. + // Mutually exclusive with BackendRef. + // +optional + // +kubebuilder:validation:Format=uri + ExternalUrl string `json:"externalUrl,omitempty"` +} + +// GuardrailBackendRef references a Kubernetes Service acting as the guardrail backend. +type GuardrailBackendRef struct { + corev1.ObjectReference `json:",inline"` + + // Port is the port number of the Kubernetes Service. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Port int32 `json:"port"` +} + +// GuardrailProviderStatus defines the observed state of GuardrailProvider. +type GuardrailProviderStatus struct { + // +operator-sdk:csv:customresourcedefinitions:type=status + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// GuardrailProvider is the Schema for the guardrailproviders API. +type GuardrailProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GuardrailProviderSpec `json:"spec,omitempty"` + Status GuardrailProviderStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// GuardrailProviderList contains a list of GuardrailProvider. +type GuardrailProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GuardrailProvider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GuardrailProvider{}, &GuardrailProviderList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2301b5a..2479138 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -880,6 +880,11 @@ func (in *AiGatewaySpec) DeepCopyInto(out *AiGatewaySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Guardrails != nil { + in, out := &in.Guardrails, &out.Guardrails + *out = make([]v1.ObjectReference, len(*in)) + copy(*out, *in) + } if in.CommonMetadata != nil { in, out := &in.CommonMetadata, &out.CommonMetadata *out = new(EmbeddedMetadata) @@ -968,6 +973,225 @@ func (in *EmbeddedMetadata) DeepCopy() *EmbeddedMetadata { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Guard) DeepCopyInto(out *Guard) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Guard. +func (in *Guard) DeepCopy() *Guard { + if in == nil { + return nil + } + out := new(Guard) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Guard) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardList) DeepCopyInto(out *GuardList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Guard, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardList. +func (in *GuardList) DeepCopy() *GuardList { + if in == nil { + return nil + } + out := new(GuardList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GuardList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardSpec) DeepCopyInto(out *GuardSpec) { + *out = *in + out.ProviderRef = in.ProviderRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardSpec. +func (in *GuardSpec) DeepCopy() *GuardSpec { + if in == nil { + return nil + } + out := new(GuardSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardStatus) DeepCopyInto(out *GuardStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardStatus. +func (in *GuardStatus) DeepCopy() *GuardStatus { + if in == nil { + return nil + } + out := new(GuardStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailBackendRef) DeepCopyInto(out *GuardrailBackendRef) { + *out = *in + out.ObjectReference = in.ObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailBackendRef. +func (in *GuardrailBackendRef) DeepCopy() *GuardrailBackendRef { + if in == nil { + return nil + } + out := new(GuardrailBackendRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailProvider) DeepCopyInto(out *GuardrailProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProvider. +func (in *GuardrailProvider) DeepCopy() *GuardrailProvider { + if in == nil { + return nil + } + out := new(GuardrailProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GuardrailProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailProviderList) DeepCopyInto(out *GuardrailProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GuardrailProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProviderList. +func (in *GuardrailProviderList) DeepCopy() *GuardrailProviderList { + if in == nil { + return nil + } + out := new(GuardrailProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GuardrailProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailProviderSpec) DeepCopyInto(out *GuardrailProviderSpec) { + *out = *in + if in.ApiKeySecretRef != nil { + in, out := &in.ApiKeySecretRef, &out.ApiKeySecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.BackendRef != nil { + in, out := &in.BackendRef, &out.BackendRef + *out = new(GuardrailBackendRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProviderSpec. +func (in *GuardrailProviderSpec) DeepCopy() *GuardrailProviderSpec { + if in == nil { + return nil + } + out := new(GuardrailProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GuardrailProviderStatus) DeepCopyInto(out *GuardrailProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProviderStatus. +func (in *GuardrailProviderStatus) DeepCopy() *GuardrailProviderStatus { + if in == nil { + return nil + } + out := new(GuardrailProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SubAgent) DeepCopyInto(out *SubAgent) { *out = *in diff --git a/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml b/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml index fcc3091..df0dc67 100644 --- a/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml +++ b/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml @@ -47,6 +47,7 @@ spec: aiModels: description: List of AI models to be made available through the gateway. items: + description: AiModel is an AI model configuration. properties: name: description: Name is the identifier for the AI model (e.g., @@ -290,6 +291,55 @@ spec: x-kubernetes-map-type: atomic type: object type: array + guardrails: + description: |- + Guardrails lists the Guard resources to be applied to requests through this AI gateway. + Guards are applied in the order they are listed. + items: + description: ObjectReference contains enough information to let + you inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: array podMetadata: description: |- PodMetadata defines labels and annotations to be applied only to the pod template diff --git a/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml b/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml new file mode 100644 index 0000000..90f057b --- /dev/null +++ b/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml @@ -0,0 +1,212 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: guardrailproviders.runtime.agentic-layer.ai +spec: + group: runtime.agentic-layer.ai + names: + kind: GuardrailProvider + listKind: GuardrailProviderList + plural: guardrailproviders + singular: guardrailprovider + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GuardrailProvider is the Schema for the guardrailproviders API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: GuardrailProviderSpec defines the desired state of GuardrailProvider. + properties: + apiKeySecretRef: + description: |- + ApiKeySecretRef references a Kubernetes Secret containing the API key for the guardrail provider. + The secret must contain the key specified in the SecretKeySelector. + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + backendRef: + description: |- + BackendRef references the Kubernetes Service acting as the guardrail backend. + When omitted, the provider uses the protocol's default managed endpoint + (e.g., the official OpenAI moderation API or AWS Bedrock). + Mutually exclusive with ExternalUrl. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + port: + description: Port is the port number of the Kubernetes Service. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + required: + - port + type: object + x-kubernetes-map-type: atomic + externalUrl: + description: |- + ExternalUrl specifies an external URL for the guardrail backend. + Use this to point to an external guardrail service outside the cluster. + Mutually exclusive with BackendRef. + format: uri + type: string + protocol: + description: Protocol defines the guardrail protocol used by this + provider. + enum: + - openai-moderation + - bedrock + type: string + transportType: + description: |- + TransportType defines the transport used to communicate with the guardrail backend. + Required when BackendRef is specified. + enum: + - http + - grpc + - envoy-ext-proc + type: string + required: + - protocol + type: object + status: + description: GuardrailProviderStatus defines the observed state of GuardrailProvider. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/runtime.agentic-layer.ai_guards.yaml b/config/crd/bases/runtime.agentic-layer.ai_guards.yaml new file mode 100644 index 0000000..cdb5899 --- /dev/null +++ b/config/crd/bases/runtime.agentic-layer.ai_guards.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: guards.runtime.agentic-layer.ai +spec: + group: runtime.agentic-layer.ai + names: + kind: Guard + listKind: GuardList + plural: guards + singular: guard + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Guard is the Schema for the guards API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: GuardSpec defines the desired state of Guard. + properties: + description: + description: |- + Description provides a human-readable description of the guard's purpose. + This field is for documentation purposes only and has no effect on the guard's behavior. + type: string + mode: + description: Mode defines when the guard is applied relative to the + LLM call. + enum: + - pre_call + - post_call + - during_call + type: string + name: + description: Name is the identifier of the guard as known by the referenced + GuardrailProvider. + minLength: 1 + type: string + providerRef: + description: |- + ProviderRef references the GuardrailProvider that hosts this guard. + If Namespace is not specified, defaults to the same namespace as the Guard. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + version: + description: Version is the version of the guard at the provider (if + supported). + type: string + required: + - mode + - name + - providerRef + type: object + status: + description: GuardStatus defines the observed state of Guard. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 2fa1fcb..d30c056 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,6 +9,8 @@ resources: - bases/runtime.agentic-layer.ai_agentruntimeconfigurations.yaml - bases/runtime.agentic-layer.ai_aigateways.yaml - bases/runtime.agentic-layer.ai_aigatewayclasses.yaml +- bases/runtime.agentic-layer.ai_guardrailproviders.yaml +- bases/runtime.agentic-layer.ai_guards.yaml - bases/runtime.agentic-layer.ai_toolgatewayclasses.yaml - bases/runtime.agentic-layer.ai_toolgateways.yaml - bases/runtime.agentic-layer.ai_toolservers.yaml diff --git a/config/rbac/guard_admin_role.yaml b/config/rbac/guard_admin_role.yaml new file mode 100644 index 0000000..e3bda57 --- /dev/null +++ b/config/rbac/guard_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over runtime.agentic-layer.ai. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guard-admin-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards + verbs: + - '*' +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards/status + verbs: + - get diff --git a/config/rbac/guard_editor_role.yaml b/config/rbac/guard_editor_role.yaml new file mode 100644 index 0000000..86fa473 --- /dev/null +++ b/config/rbac/guard_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the runtime.agentic-layer.ai. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guard-editor-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards/status + verbs: + - get diff --git a/config/rbac/guard_viewer_role.yaml b/config/rbac/guard_viewer_role.yaml new file mode 100644 index 0000000..97fc803 --- /dev/null +++ b/config/rbac/guard_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to runtime.agentic-layer.ai resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guard-viewer-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards + verbs: + - get + - list + - watch +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guards/status + verbs: + - get diff --git a/config/rbac/guardrailprovider_admin_role.yaml b/config/rbac/guardrailprovider_admin_role.yaml new file mode 100644 index 0000000..5744dc2 --- /dev/null +++ b/config/rbac/guardrailprovider_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over runtime.agentic-layer.ai. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guardrailprovider-admin-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders + verbs: + - '*' +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders/status + verbs: + - get diff --git a/config/rbac/guardrailprovider_editor_role.yaml b/config/rbac/guardrailprovider_editor_role.yaml new file mode 100644 index 0000000..43eeea2 --- /dev/null +++ b/config/rbac/guardrailprovider_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the runtime.agentic-layer.ai. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guardrailprovider-editor-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders/status + verbs: + - get diff --git a/config/rbac/guardrailprovider_viewer_role.yaml b/config/rbac/guardrailprovider_viewer_role.yaml new file mode 100644 index 0000000..5ede1e4 --- /dev/null +++ b/config/rbac/guardrailprovider_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project agent-runtime-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to runtime.agentic-layer.ai resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: agent-runtime-operator + app.kubernetes.io/managed-by: kustomize + name: guardrailprovider-viewer-role +rules: +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders + verbs: + - get + - list + - watch +- apiGroups: + - runtime.agentic-layer.ai + resources: + - guardrailproviders/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 507a68b..40e2ae8 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -34,5 +34,11 @@ resources: - agent_admin_role.yaml - agent_editor_role.yaml - agent_viewer_role.yaml +- guard_admin_role.yaml +- guard_editor_role.yaml +- guard_viewer_role.yaml +- guardrailprovider_admin_role.yaml +- guardrailprovider_editor_role.yaml +- guardrailprovider_viewer_role.yaml diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index cbfe02f..9ae10d3 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,4 +4,6 @@ resources: - runtime_v1alpha1_agenticworkforce.yaml - runtime_v1alpha1_agentruntimeconfiguration.yaml - runtime_v1alpha1_toolserver.yaml +- runtime_v1alpha1_guardrailprovider.yaml +- runtime_v1alpha1_guard.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/runtime_v1alpha1_guard.yaml b/config/samples/runtime_v1alpha1_guard.yaml new file mode 100644 index 0000000..4a38c8b --- /dev/null +++ b/config/samples/runtime_v1alpha1_guard.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: Guard +metadata: + name: pii-guard + namespace: default +spec: + name: pii-detection + version: "1.0.0" + mode: pre_call + description: Detects and handles Personally Identifiable Information (PII) in user inputs. + providerRef: + kind: GuardrailProvider + name: openai-moderation + namespace: default +--- +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: Guard +metadata: + name: toxic-language-guard + namespace: default +spec: + name: toxic-language + mode: pre_call + description: Detects and blocks toxic or harmful language in user inputs. + providerRef: + kind: GuardrailProvider + name: openai-moderation + namespace: default diff --git a/config/samples/runtime_v1alpha1_guardrailprovider.yaml b/config/samples/runtime_v1alpha1_guardrailprovider.yaml new file mode 100644 index 0000000..a71a119 --- /dev/null +++ b/config/samples/runtime_v1alpha1_guardrailprovider.yaml @@ -0,0 +1,41 @@ +--- +# Create the API key secret before applying this sample: +# kubectl create secret generic openai-api-key --from-literal=api-key= +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: GuardrailProvider +metadata: + name: openai-moderation + namespace: default +spec: + protocol: openai-moderation + apiKeySecretRef: + name: openai-api-key + key: api-key +--- +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: GuardrailProvider +metadata: + name: custom-openai-moderation + namespace: default +spec: + protocol: openai-moderation + transportType: http + backendRef: + kind: Service + name: custom-guardrail-backend + namespace: default + port: 8080 +--- +# Create the API key secret before applying this sample: +# kubectl create secret generic guardrail-api-key --from-literal=api-key= +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: GuardrailProvider +metadata: + name: external-guardrail-provider + namespace: default +spec: + protocol: openai-moderation + externalUrl: https://api.example.com/guardrails + apiKeySecretRef: + name: guardrail-api-key + key: api-key diff --git a/go.sum b/go.sum index aee18b0..779e524 100644 --- a/go.sum +++ b/go.sum @@ -243,8 +243,6 @@ k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= @@ -253,8 +251,6 @@ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzk k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= -sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/test/e2e/guardrail_e2e_test.go b/test/e2e/guardrail_e2e_test.go new file mode 100644 index 0000000..9e89417 --- /dev/null +++ b/test/e2e/guardrail_e2e_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2025. + +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 e2e + +import ( + "os/exec" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/agentic-layer/agent-runtime-operator/test/utils" +) + +var _ = Describe("Guardrails", Ordered, func() { + const ( + providerSampleFile = "config/samples/runtime_v1alpha1_guardrailprovider.yaml" + guardSampleFile = "config/samples/runtime_v1alpha1_guard.yaml" + guardrailNamespace = "default" + ) + + BeforeAll(func() { + By("applying the guardrailprovider sample") + _, err := utils.Run(exec.Command("kubectl", "apply", "-f", providerSampleFile)) + Expect(err).NotTo(HaveOccurred(), "Failed to apply guardrailprovider sample") + + By("applying the guard sample") + _, err = utils.Run(exec.Command("kubectl", "apply", "-f", guardSampleFile)) + Expect(err).NotTo(HaveOccurred(), "Failed to apply guard sample") + }) + + AfterAll(func() { + By("cleaning up the guard sample") + _, _ = utils.Run(exec.Command("kubectl", "delete", "-f", guardSampleFile, "--ignore-not-found=true")) + + By("cleaning up the guardrailprovider sample") + _, _ = utils.Run(exec.Command("kubectl", "delete", "-f", providerSampleFile, "--ignore-not-found=true")) + }) + + It("should successfully apply GuardrailProvider resources", func() { + By("verifying openai-moderation provider exists") + _, err := utils.Run(exec.Command("kubectl", "get", "guardrailprovider", "openai-moderation", + "-n", guardrailNamespace)) + Expect(err).NotTo(HaveOccurred(), "openai-moderation GuardrailProvider should exist") + + By("verifying custom-openai-moderation provider exists") + _, err = utils.Run(exec.Command("kubectl", "get", "guardrailprovider", "custom-openai-moderation", + "-n", guardrailNamespace)) + Expect(err).NotTo(HaveOccurred(), "custom-openai-moderation GuardrailProvider should exist") + + By("verifying external-guardrail-provider exists") + _, err = utils.Run(exec.Command("kubectl", "get", "guardrailprovider", "external-guardrail-provider", + "-n", guardrailNamespace)) + Expect(err).NotTo(HaveOccurred(), "external-guardrail-provider GuardrailProvider should exist") + }) + + It("should successfully apply Guard resources", func() { + By("verifying pii-guard exists") + _, err := utils.Run(exec.Command("kubectl", "get", "guard", "pii-guard", + "-n", guardrailNamespace)) + Expect(err).NotTo(HaveOccurred(), "pii-guard Guard should exist") + + By("verifying toxic-language-guard exists") + _, err = utils.Run(exec.Command("kubectl", "get", "guard", "toxic-language-guard", + "-n", guardrailNamespace)) + Expect(err).NotTo(HaveOccurred(), "toxic-language-guard Guard should exist") + }) +})