From 7e7590549fbaa93da8cf94cf74c2cb81a19d19b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:13:00 +0000 Subject: [PATCH 1/4] Initial plan From e4ffdb1b05136f4c79d681107bf3a8ea74cca50d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:28:32 +0000 Subject: [PATCH 2/4] feat: introduce Guard and GuardrailProvider CRDs with AiGateway integration Co-authored-by: g3force <779094+g3force@users.noreply.github.com> --- PROJECT | 16 ++ api/v1alpha1/aigateway_types.go | 17 ++ api/v1alpha1/guard_types.go | 88 +++++++ api/v1alpha1/guardrailprovider_types.go | 88 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 248 ++++++++++++++++++ .../runtime.agentic-layer.ai_aigateways.yaml | 20 ++ ...e.agentic-layer.ai_guardrailproviders.yaml | 150 +++++++++++ .../runtime.agentic-layer.ai_guards.yaml | 150 +++++++++++ config/rbac/guard_admin_role.yaml | 27 ++ config/rbac/guard_editor_role.yaml | 33 +++ config/rbac/guard_viewer_role.yaml | 29 ++ config/rbac/guardrailprovider_admin_role.yaml | 27 ++ .../rbac/guardrailprovider_editor_role.yaml | 33 +++ .../rbac/guardrailprovider_viewer_role.yaml | 29 ++ config/rbac/kustomization.yaml | 6 + config/samples/kustomization.yaml | 2 + config/samples/runtime_v1alpha1_guard.yaml | 27 ++ .../runtime_v1alpha1_guardrailprovider.yaml | 21 ++ go.sum | 4 - 19 files changed, 1011 insertions(+), 4 deletions(-) create mode 100644 api/v1alpha1/guard_types.go create mode 100644 api/v1alpha1/guardrailprovider_types.go create mode 100644 config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml create mode 100644 config/crd/bases/runtime.agentic-layer.ai_guards.yaml create mode 100644 config/rbac/guard_admin_role.yaml create mode 100644 config/rbac/guard_editor_role.yaml create mode 100644 config/rbac/guard_viewer_role.yaml create mode 100644 config/rbac/guardrailprovider_admin_role.yaml create mode 100644 config/rbac/guardrailprovider_editor_role.yaml create mode 100644 config/rbac/guardrailprovider_viewer_role.yaml create mode 100644 config/samples/runtime_v1alpha1_guard.yaml create mode 100644 config/samples/runtime_v1alpha1_guardrailprovider.yaml 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..c06b778 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 []GuardRef `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,18 @@ type AiGatewaySpec struct { PodMetadata *EmbeddedMetadata `json:"podMetadata,omitempty"` } +// GuardRef is a reference to a Guard resource. +type GuardRef struct { + // Name is the name of the Guard resource. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Namespace is the namespace of the Guard resource. + // If not specified, defaults to the same namespace as the AiGateway. + // +optional + Namespace string `json:"namespace,omitempty"` +} + 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..ed68808 --- /dev/null +++ b/api/v1alpha1/guard_types.go @@ -0,0 +1,88 @@ +/* +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 ( + 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 GuardrailProviderRef `json:"providerRef"` +} + +// GuardrailProviderRef is a reference to a GuardrailProvider resource. +type GuardrailProviderRef struct { + // Name is the name of the GuardrailProvider. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Namespace is the namespace of the GuardrailProvider. + // If not specified, defaults to the same namespace as the Guard. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// 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..a05914f --- /dev/null +++ b/api/v1alpha1/guardrailprovider_types.go @@ -0,0 +1,88 @@ +/* +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 ( + 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"` + + // 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). + // +optional + BackendRef *GuardrailBackendRef `json:"backendRef,omitempty"` +} + +// GuardrailBackendRef references a Kubernetes Service acting as the guardrail backend. +type GuardrailBackendRef struct { + // Name is the name of the Kubernetes Service. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Namespace is the namespace of the Kubernetes Service. + // If not specified, defaults to the same namespace as the GuardrailProvider. + // +optional + Namespace string `json:"namespace,omitempty"` + + // 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..23d156e 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([]GuardRef, len(*in)) + copy(*out, *in) + } if in.CommonMetadata != nil { in, out := &in.CommonMetadata, &out.CommonMetadata *out = new(EmbeddedMetadata) @@ -968,6 +973,249 @@ 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 *GuardRef) DeepCopyInto(out *GuardRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardRef. +func (in *GuardRef) DeepCopy() *GuardRef { + if in == nil { + return nil + } + out := new(GuardRef) + in.DeepCopyInto(out) + return out +} + +// 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 +} + +// 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 *GuardrailProviderRef) DeepCopyInto(out *GuardrailProviderRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProviderRef. +func (in *GuardrailProviderRef) DeepCopy() *GuardrailProviderRef { + if in == nil { + return nil + } + out := new(GuardrailProviderRef) + in.DeepCopyInto(out) + return out +} + +// 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.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..bde1198 100644 --- a/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml +++ b/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml @@ -290,6 +290,26 @@ 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: GuardRef is a reference to a Guard resource. + properties: + name: + description: Name is the name of the Guard resource. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the Guard resource. + If not specified, defaults to the same namespace as the AiGateway. + type: string + required: + - name + type: object + 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..51b6dfe --- /dev/null +++ b/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml @@ -0,0 +1,150 @@ +--- +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: + 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). + properties: + name: + description: Name is the name of the Kubernetes Service. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the Kubernetes Service. + If not specified, defaults to the same namespace as the GuardrailProvider. + type: string + port: + description: Port is the port number of the Kubernetes Service. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + - port + type: object + 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..dd68be5 --- /dev/null +++ b/config/crd/bases/runtime.agentic-layer.ai_guards.yaml @@ -0,0 +1,150 @@ +--- +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: + name: + description: Name is the name of the GuardrailProvider. + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the GuardrailProvider. + If not specified, defaults to the same namespace as the Guard. + type: string + required: + - name + type: object + 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/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..c280d00 --- /dev/null +++ b/config/samples/runtime_v1alpha1_guard.yaml @@ -0,0 +1,27 @@ +--- +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: + 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: + 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..a6ebe88 --- /dev/null +++ b/config/samples/runtime_v1alpha1_guardrailprovider.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: GuardrailProvider +metadata: + name: openai-moderation + namespace: default +spec: + protocol: openai-moderation +--- +apiVersion: runtime.agentic-layer.ai/v1alpha1 +kind: GuardrailProvider +metadata: + name: custom-openai-moderation + namespace: default +spec: + protocol: openai-moderation + transportType: http + backendRef: + name: custom-guardrail-backend + namespace: default + port: 8080 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= From a4730cfa499ace14ec7b2929ff2f6ed415729af0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:56:11 +0000 Subject: [PATCH 3/4] feat: address PR review comments - use standard object references, add API key and external URL fields, add e2e tests Co-authored-by: g3force <779094+g3force@users.noreply.github.com> --- api/v1alpha1/aigateway_types.go | 15 +--- api/v1alpha1/guard_types.go | 15 +--- api/v1alpha1/guardrailprovider_types.go | 23 ++++-- api/v1alpha1/zz_generated.deepcopy.go | 38 ++------- .../runtime.agentic-layer.ai_aigateways.yaml | 44 ++++++++-- ...e.agentic-layer.ai_guardrailproviders.yaml | 72 +++++++++++++++-- .../runtime.agentic-layer.ai_guards.yaml | 40 +++++++-- config/samples/runtime_v1alpha1_guard.yaml | 2 + .../runtime_v1alpha1_guardrailprovider.yaml | 20 +++++ test/e2e/guardrail_e2e_test.go | 81 +++++++++++++++++++ 10 files changed, 267 insertions(+), 83 deletions(-) create mode 100644 test/e2e/guardrail_e2e_test.go diff --git a/api/v1alpha1/aigateway_types.go b/api/v1alpha1/aigateway_types.go index c06b778..fd2c963 100644 --- a/api/v1alpha1/aigateway_types.go +++ b/api/v1alpha1/aigateway_types.go @@ -51,7 +51,7 @@ type AiGatewaySpec struct { // 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 []GuardRef `json:"guardrails,omitempty"` + 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. @@ -64,18 +64,7 @@ type AiGatewaySpec struct { PodMetadata *EmbeddedMetadata `json:"podMetadata,omitempty"` } -// GuardRef is a reference to a Guard resource. -type GuardRef struct { - // Name is the name of the Guard resource. - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // Namespace is the namespace of the Guard resource. - // If not specified, defaults to the same namespace as the AiGateway. - // +optional - Namespace string `json:"namespace,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 index ed68808..0c6c17b 100644 --- a/api/v1alpha1/guard_types.go +++ b/api/v1alpha1/guard_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -41,19 +42,7 @@ type GuardSpec struct { // ProviderRef references the GuardrailProvider that hosts this guard. // If Namespace is not specified, defaults to the same namespace as the Guard. - ProviderRef GuardrailProviderRef `json:"providerRef"` -} - -// GuardrailProviderRef is a reference to a GuardrailProvider resource. -type GuardrailProviderRef struct { - // Name is the name of the GuardrailProvider. - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // Namespace is the namespace of the GuardrailProvider. - // If not specified, defaults to the same namespace as the Guard. - // +optional - Namespace string `json:"namespace,omitempty"` + ProviderRef corev1.ObjectReference `json:"providerRef"` } // GuardStatus defines the observed state of Guard. diff --git a/api/v1alpha1/guardrailprovider_types.go b/api/v1alpha1/guardrailprovider_types.go index a05914f..e4f8fa8 100644 --- a/api/v1alpha1/guardrailprovider_types.go +++ b/api/v1alpha1/guardrailprovider_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -26,6 +27,11 @@ type GuardrailProviderSpec struct { // +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 @@ -35,20 +41,21 @@ type GuardrailProviderSpec struct { // 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 { - // Name is the name of the Kubernetes Service. - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // Namespace is the namespace of the Kubernetes Service. - // If not specified, defaults to the same namespace as the GuardrailProvider. - // +optional - Namespace string `json:"namespace,omitempty"` + corev1.ObjectReference `json:",inline"` // Port is the port number of the Kubernetes Service. // +kubebuilder:validation:Minimum=1 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 23d156e..2479138 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -882,7 +882,7 @@ func (in *AiGatewaySpec) DeepCopyInto(out *AiGatewaySpec) { } if in.Guardrails != nil { in, out := &in.Guardrails, &out.Guardrails - *out = make([]GuardRef, len(*in)) + *out = make([]v1.ObjectReference, len(*in)) copy(*out, *in) } if in.CommonMetadata != nil { @@ -1032,21 +1032,6 @@ func (in *GuardList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardRef) DeepCopyInto(out *GuardRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardRef. -func (in *GuardRef) DeepCopy() *GuardRef { - if in == nil { - return nil - } - out := new(GuardRef) - in.DeepCopyInto(out) - return out -} - // 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 @@ -1088,6 +1073,7 @@ func (in *GuardStatus) DeepCopy() *GuardStatus { // 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. @@ -1159,24 +1145,14 @@ func (in *GuardrailProviderList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GuardrailProviderRef) DeepCopyInto(out *GuardrailProviderRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuardrailProviderRef. -func (in *GuardrailProviderRef) DeepCopy() *GuardrailProviderRef { - if in == nil { - return nil - } - out := new(GuardrailProviderRef) - in.DeepCopyInto(out) - return out -} - // 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) diff --git a/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml b/config/crd/bases/runtime.agentic-layer.ai_aigateways.yaml index bde1198..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., @@ -295,20 +296,49 @@ spec: 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: GuardRef is a reference to a Guard resource. + 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 is the name of the Guard resource. - minLength: 1 + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string namespace: description: |- - Namespace is the namespace of the Guard resource. - If not specified, defaults to the same namespace as the AiGateway. + 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 - required: - - name type: object + x-kubernetes-map-type: atomic type: array podMetadata: description: |- diff --git a/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml b/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml index 51b6dfe..90f057b 100644 --- a/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml +++ b/config/crd/bases/runtime.agentic-layer.ai_guardrailproviders.yaml @@ -39,20 +39,65 @@ spec: 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 is the name of the Kubernetes Service. - minLength: 1 + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string namespace: description: |- - Namespace is the namespace of the Kubernetes Service. - If not specified, defaults to the same namespace as the GuardrailProvider. + 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. @@ -60,10 +105,27 @@ spec: 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: - - name - 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. diff --git a/config/crd/bases/runtime.agentic-layer.ai_guards.yaml b/config/crd/bases/runtime.agentic-layer.ai_guards.yaml index dd68be5..cdb5899 100644 --- a/config/crd/bases/runtime.agentic-layer.ai_guards.yaml +++ b/config/crd/bases/runtime.agentic-layer.ai_guards.yaml @@ -62,18 +62,46 @@ spec: 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 is the name of the GuardrailProvider. - minLength: 1 + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string namespace: description: |- - Namespace is the namespace of the GuardrailProvider. - If not specified, defaults to the same namespace as the Guard. + 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 - required: - - name type: object + x-kubernetes-map-type: atomic version: description: Version is the version of the guard at the provider (if supported). diff --git a/config/samples/runtime_v1alpha1_guard.yaml b/config/samples/runtime_v1alpha1_guard.yaml index c280d00..4a38c8b 100644 --- a/config/samples/runtime_v1alpha1_guard.yaml +++ b/config/samples/runtime_v1alpha1_guard.yaml @@ -10,6 +10,7 @@ spec: mode: pre_call description: Detects and handles Personally Identifiable Information (PII) in user inputs. providerRef: + kind: GuardrailProvider name: openai-moderation namespace: default --- @@ -23,5 +24,6 @@ spec: 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 index a6ebe88..a71a119 100644 --- a/config/samples/runtime_v1alpha1_guardrailprovider.yaml +++ b/config/samples/runtime_v1alpha1_guardrailprovider.yaml @@ -1,4 +1,6 @@ --- +# 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: @@ -6,6 +8,9 @@ metadata: namespace: default spec: protocol: openai-moderation + apiKeySecretRef: + name: openai-api-key + key: api-key --- apiVersion: runtime.agentic-layer.ai/v1alpha1 kind: GuardrailProvider @@ -16,6 +21,21 @@ 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/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") + }) +}) From 5d379a0d11ba8d8caad7c0469e4c7fec8ca9f968 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:41:17 +0000 Subject: [PATCH 4/4] fix: add Guard and GuardrailProvider CRDs to kustomization to fix e2e test failures Co-authored-by: g3force <779094+g3force@users.noreply.github.com> --- config/crd/kustomization.yaml | 2 ++ 1 file changed, 2 insertions(+) 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