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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{- if .Values.vap.agentLabelProtection.enable }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: agent-label-protection
labels:
{{- include "chart.labels" . | nindent 4 }}
app.kubernetes.io/component: admission
spec:
policyName: agent-label-protection
validationActions: [Deny]
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{{- if .Values.vap.agentLabelProtection.enable }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: agent-label-protection
labels:
{{- include "chart.labels" . | nindent 4 }}
app.kubernetes.io/component: admission
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "statefulsets"]

matchConditions:
- name: 'has-agent-type-label'
expression: >-
has(object.metadata.labels) && 'kagenti.io/type' in object.metadata.labels

- name: 'not-exempt-service-account'
expression: >-
!(request.userInfo.username == 'system:serviceaccount:{{ .Release.Namespace }}:{{ .Values.controllerManager.serviceAccountName }}')
{{- range .Values.vap.agentLabelProtection.exemptServiceAccounts }}
&& !(request.userInfo.username == 'system:serviceaccount:{{ . }}')
{{- end }}

variables:
- name: labelUnchanged
expression: >-
request.operation == 'UPDATE'
&& has(oldObject.metadata.labels)
&& 'kagenti.io/type' in oldObject.metadata.labels
&& oldObject.metadata.labels['kagenti.io/type'] == object.metadata.labels['kagenti.io/type']

validations:
- expression: "variables.labelUnchanged"
messageExpression: >-
'The kagenti.io/type label on '
+ object.metadata.namespace + '/' + object.metadata.name
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
reason: Forbidden
{{- end }}
10 changes: 10 additions & 0 deletions charts/kagenti-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ metrics:
webhook:
enable: true

# [VAP]: ValidatingAdmissionPolicy for agent label protection.
# Prevents manual application of the kagenti.io/type label on Deployments and
# StatefulSets. Only the backend UI and the operator SAs (derived from
# controllerManager.serviceAccountName and the release namespace) are exempt.
vap:
agentLabelProtection:
enable: true
exemptServiceAccounts:
- "kagenti-system:kagenti-backend"

# [PROMETHEUS]: To enable a ServiceMonitor to export metrics to Prometheus set true
prometheus:
enable: false
Expand Down
106 changes: 19 additions & 87 deletions kagenti-operator/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ This scenario demonstrates the complete lifecycle of an AI agent deployment on t
## Overview

### Kagenti Operator
The Kagenti Operator discovers, indexes, and secures AI agents deployed in Kubernetes. There are two ways to enroll workloads:
The Kagenti Operator discovers, indexes, and secures AI agents deployed in Kubernetes. Enroll workloads by creating an `AgentRuntime` CR:

1. **AgentRuntime CR (Recommended)** — Create a Deployment with a `protocol.kagenti.io/a2a` label and an `AgentRuntime` CR pointing to it. The controller applies `kagenti.io/type` labels and triggers sidecar injection automatically. The protocol label enables automatic AgentCard creation for agent discovery.
2. **Manual labels** — Add the `kagenti.io/type: agent` label directly to your Deployment or StatefulSet. This is simpler for quick tests but does not provide identity or observability configuration.
1. Create a Deployment with a `protocol.kagenti.io/a2a` label
2. Create an `AgentRuntime` CR pointing to it
3. The controller applies `kagenti.io/type` labels and triggers sidecar injection automatically
4. The protocol label enables automatic AgentCard creation for agent discovery

A `ValidatingAdmissionPolicy` prevents the `kagenti.io/type` label from being set directly on Deployments or StatefulSets — it can only be applied by the operator via an AgentRuntime CR.

> **Note:** The `Agent` Custom Resource is deprecated and will be removed in a future release.

Expand Down Expand Up @@ -180,86 +184,6 @@ kubectl delete agentruntime weather-agent-runtime -n team1

---

## Deploy an Agent with Manual Labels (Alternative)

Deploy an agent as a standard Kubernetes Deployment with the required `kagenti.io/type: agent` label. The operator will automatically discover the workload and create an AgentCard for it. This approach does not provide AgentRuntime's identity or observability configuration.

### Quick Example Deployment

```yaml
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-agent
namespace: team1
labels:
app.kubernetes.io/name: weather-agent
kagenti.io/type: agent
protocol.kagenti.io/a2a: ""
kagenti.io/framework: LangGraph
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: weather-agent
template:
metadata:
labels:
app.kubernetes.io/name: weather-agent
kagenti.io/type: agent
spec:
containers:
- name: agent
image: "ghcr.io/kagenti/agent-examples/weather_service:v0.0.1-alpha.3"
ports:
- containerPort: 8000
imagePullPolicy: Always
env:
- name: PORT
value: "8000"
- name: UV_CACHE_DIR
value: /app/.cache/uv
- name: MCP_URL
value: http://weather-tool-mcp.team1.svc.cluster.local:8000/mcp
- name: LLM_API_BASE
value: http://host.docker.internal:11434/v1
- name: LLM_API_KEY
value: dummy
- name: LLM_MODEL
value: llama3.2:3b-instruct-fp16
---
apiVersion: v1
kind: Service
metadata:
name: weather-agent
namespace: team1
spec:
selector:
app.kubernetes.io/name: weather-agent
ports:
- name: http
port: 8000
targetPort: 8000
EOF

```

**Check Status**:
```bash

# Check discovered agent cards
kubectl get agentcards -n team1

# Check deployment status
kubectl get deployment weather-agent -n team1

# View logs
kubectl logs -l app.kubernetes.io/name=weather-agent -n team1
```

---

## Deploy an MCP Server

MCP (Model Context Protocol) servers provide tools and resources that your agents can use. Deploy an MCP server after your agent is running.
Expand All @@ -273,18 +197,15 @@ metadata:
name: weather-tool
labels:
app.kubernetes.io/name: weather-tool
kagenti.io/type: tool
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: weather-tool
kagenti.io/type: tool
template:
metadata:
labels:
app.kubernetes.io/name: weather-tool
kagenti.io/type: tool
spec:
containers:
- name: mcp
Expand All @@ -306,6 +227,18 @@ spec:
- name: cache
emptyDir: {}
---
apiVersion: agent.kagenti.dev/v1alpha1
kind: AgentRuntime
metadata:
name: weather-tool
namespace: team1
spec:
type: tool
targetRef:
apiVersion: apps/v1
kind: Deployment
name: weather-tool
---
apiVersion: v1
kind: Service
metadata:
Expand All @@ -315,7 +248,6 @@ metadata:
app.kubernetes.io/name: weather-service
kagenti.io/framework: LangGraph
kagenti.io/inject: enabled
kagenti.io/type: agent
kagenti.io/workload-type: deployment
protocol.kagenti.io/a2a: ""
name: weather-service
Expand Down
9 changes: 0 additions & 9 deletions kagenti-operator/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,15 +627,6 @@ func main() {
os.Exit(1)
}

// Defaults-only config reconciler: propagates ConfigMap changes to
// workloads that have kagenti.io/type but no AgentRuntime CR.
if err = (&injector.DefaultsConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DefaultsConfig")
os.Exit(1)
}
}
// +kubebuilder:scaffold:builder

Expand Down
2 changes: 2 additions & 0 deletions kagenti-operator/config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ resources:
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [VAP] ValidatingAdmissionPolicy that prevents manual application of kagenti.io/type labels.
- ../vap
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ metadata:
name: my-agent
labels:
app.kubernetes.io/name: my-agent
kagenti.io/type: agent
protocol.kagenti.io/a2a: ""
annotations:
kagenti.io/skills: '["openshift-review"]'
Expand Down
3 changes: 3 additions & 0 deletions kagenti-operator/config/vap/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resources:
- validating-admission-policy.yaml
- validating-admission-policy-binding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: agent-label-protection
labels:
app.kubernetes.io/name: kagenti-operator
app.kubernetes.io/component: admission
spec:
policyName: agent-label-protection
validationActions: [Deny]
66 changes: 66 additions & 0 deletions kagenti-operator/config/vap/validating-admission-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ValidatingAdmissionPolicy: prevent manual application of the kagenti.io/type
# label on Deployments and StatefulSets. Only the kagenti-operator controller
# (via an AgentRuntime CR) and the UI backend are allowed to set this label.
# Users who need the label must create an AgentRuntime targeting their workload
# or use the UI.
#
# The policy is split into two layers:
#
# matchConditions — skip evaluation entirely when:
# 1. The object does not carry kagenti.io/type (irrelevant requests), OR
# 2. The request originates from the operator's or UI backend's service account.
# Two operator SA identities are listed because kustomize (make deploy)
# and Helm use different namespaces and SA names:
# - kagenti-operator-system/kagenti-operator-controller-manager (kustomize)
# - kagenti-system/controller-manager (Helm)
#
# validations — for everyone else, allow only UPDATE requests where the
# label already existed on the previous revision with the same value
# (i.e. the user is modifying other fields but leaving the label intact).
#
# CREATE requests that carry the label are always rejected for non-operator
# callers, because the "label unchanged" check evaluates to false on CREATE
# (there is no oldObject).
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: agent-label-protection
labels:
app.kubernetes.io/name: kagenti-operator
app.kubernetes.io/component: admission
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments", "statefulsets"]

matchConditions:
- name: 'has-agent-type-label'
expression: >-
has(object.metadata.labels) && 'kagenti.io/type' in object.metadata.labels

- name: 'not-operator-or-ui-backend-service-account'
expression: >-
!(request.userInfo.username == 'system:serviceaccount:kagenti-operator-system:kagenti-operator-controller-manager')
&& !(request.userInfo.username == 'system:serviceaccount:kagenti-system:controller-manager')
&& !(request.userInfo.username == 'system:serviceaccount:kagenti-system:kagenti-backend')

variables:
- name: labelUnchanged
expression: >-
request.operation == 'UPDATE'
&& has(oldObject.metadata.labels)
&& 'kagenti.io/type' in oldObject.metadata.labels
&& oldObject.metadata.labels['kagenti.io/type'] == object.metadata.labels['kagenti.io/type']

validations:
- expression: "variables.labelUnchanged"
messageExpression: >-
'The kagenti.io/type label on '
+ object.metadata.namespace + '/' + object.metadata.name
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
reason: Forbidden
Loading
Loading