Skip to content
Open
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
9 changes: 5 additions & 4 deletions STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,11 @@ All new CRD API surface goes in `v1alpha3`.
// +kubebuilder:validation:XValidation:message="only one of stdio or http may be set",rule="[has(self.stdio), has(self.http)].filter(x, x).size() <= 1"
```

- Use standard K8s types before inventing new ones: `[]metav1.Condition` for
status (with `meta.SetStatusCondition`), `corev1.LocalObjectReference` /
`TypedLocalObjectReference` for references. Start with single-namespace
(local) references; widen later only if needed.
- Use standard K8s types before inventing new ones, such as `[]metav1.Condition`
for status (with `meta.SetStatusCondition`).
- References use kagent-owned types, not `corev1.LocalObjectReference` or
`TypedObjectReference`, because kagent can't evolve a type it doesn't own.
Start with single-namespace (local) references; widen later only if needed.
- References **MUST** be structured fields, not opaque `"namespace/name"`
strings that callers parse.
- Keep the API small: don't add convenience/sugar fields to status that can be
Expand Down
22 changes: 19 additions & 3 deletions docs/architecture/cross-namespace-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Agents can use ModelConfigs and RemoteMCPServers in other namespaces. A reference
contains `name` and optionally `namespace`; omitting or leaving namespace empty
uses the namespace of the reference owner. Names never contain `namespace/name`.
A model or MCP server reference may also set `kind`, and `group` with it.
Omitted values select ModelConfig or RemoteMCPServer; the controller rejects
any other kind with `ResolvedRefs=False` and reason `InvalidKind`.

AgentTemplate owns its model and MCP references, including child templates.
Inline templates use the Agent namespace. Harness owns memory and summarizer
Expand Down Expand Up @@ -152,7 +155,20 @@ preserves namespaces when saving references.

## Rollout

Install the regenerated CRDs **before** upgrading the controller or applying
Install the regenerated CRDs **before** you upgrade the controller or apply
namespace-qualified manifests. Older schemas can prune the namespace fields.
Existing local manifests retain their JSON shape. Go callers must adopt
`v1alpha3.ObjectReference` and `corev1.TypedObjectReference` for the changed fields.

Manifests and stored objects from v1.0.x work unchanged and keep their revision
digest.

- **MCP server `kind`** defaults to `RemoteMCPServer` and is stored, so v1.0.x
controllers and clients can still update the object.
- **Other kinds** on model references need the new controller and CRDs. A v1.0.x
controller ignores them, and a CRD rollback drops them, so the reference selects
the ModelConfig with the same name. Before you roll back to v1.0.x, point those
references back to the default kind.
- **`apiGroup`** stays as a deprecated MCP server field that must be omitted. The
API server drops the `null` that v1.0.x Go clients send.

In Go, `v1alpha3.ObjectReference` is renamed `ModelConfigReference`, and
`MCPToolBinding.Server` is now an `MCPServerReference`.
16 changes: 11 additions & 5 deletions docs/kubernetes-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ details out of the public API unless callers need them to express intent.

Namespace and name identify an object; UID distinguishes deletion and recreation.
Validate names according to the referenced kind, not a universal DNS-label rule.
Use native structured types such as `LocalObjectReference`,
`TypedLocalObjectReference`, and `SecretKeySelector`. Start with same-namespace
references; crossing namespaces requires an explicit authorization policy.
Own reference types instead of reusing `corev1` ones such as
`LocalObjectReference` and `TypedObjectReference`. kagent can't add fields,
defaults, or validation to a type it doesn't own, and native semantics can
differ: `TypedObjectReference` treats an omitted `apiGroup` as the core group.
Model and MCP server references own `group` and `kind` fields that default to the
kagent kind, and the controller rejects other kinds with reason `InvalidKind`.
Existing `corev1` reference fields keep their type until they need to change.
Start with same-namespace references; crossing namespaces requires an explicit
authorization policy.

Do not encode references as `"namespace/name"` strings. Keep inline configuration
and references in separate, typed fields. If both forms are supported, define
Expand All @@ -50,8 +56,8 @@ owning boundary. An object reference grants no access by itself.
pointer wrappers. Requiredness must be explicit in the generated schema.
- Declare defaults and bounds with kubebuilder markers. Explain the meaning of
absent, empty, and zero values; defaults must pass validation.
- Prefer native Kubernetes quantities, selectors, references, and conditions
when their semantics match.
- Prefer native Kubernetes quantities, selectors, and conditions when their
semantics match.
- Declare list/map semantics, keys, and uniqueness where merge behavior matters.
Use inline embedded structs for shared sub-specs.
- Interfaces in API packages carry `+kubebuilder:object:generate=false`.
Expand Down
10 changes: 3 additions & 7 deletions go/api/v1alpha3/agenttemplate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,9 @@ type AgentTemplatePromptSource struct {

// MCPToolBinding binds tools from an authorized MCP server.
type MCPToolBinding struct {
// +kubebuilder:validation:XValidation:rule="self.kind == 'RemoteMCPServer'",message="kind must be RemoteMCPServer"
// +kubebuilder:validation:XValidation:rule="!has(self.apiGroup)",message="apiGroup must be omitted"
// +kubebuilder:validation:XValidation:rule="size(self.name) > 0 && size(self.name) <= 253",message="name must contain between 1 and 253 characters",fieldPath=".name"
// +kubebuilder:validation:XValidation:rule="!has(self.namespace) || size(self.namespace) <= 63",message="namespace must contain at most 63 characters",fieldPath=".namespace"
// Server is the MCP server that provides the tools.
// +required
Server corev1.TypedObjectReference `json:"server"`
Comment thread
codyhartsook marked this conversation as resolved.
Resolved
Server MCPServerReference `json:"server"`
// Tools optionally limits which server tools are exposed. An omitted or empty
// list exposes every tool. Harnesses that cannot enforce a partial selection
// may expose the whole server and report a warning.
Expand Down Expand Up @@ -176,9 +173,8 @@ type PluginBundle struct {
// +kubebuilder:validation:XValidation:rule="!has(self.outputSchemaStrict) || !self.outputSchemaStrict || has(self.outputSchema) || has(self.outputSchemaFrom)",message="outputSchemaStrict requires outputSchema or outputSchemaFrom"
type AgentTemplateSpec struct {
// ModelConfig is required by managed harnesses and optional for BYO harnesses.
// +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="name must not be empty"
// +optional
ModelConfig *ObjectReference `json:"modelConfig,omitempty"`
ModelConfig *ModelConfigReference `json:"modelConfig,omitempty"`
// +optional
Description string `json:"description,omitempty"`
// +optional
Expand Down
88 changes: 80 additions & 8 deletions go/api/v1alpha3/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,105 @@ limitations under the License.
package v1alpha3

import (
"cmp"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
)

// ObjectReference identifies a resource; an omitted namespace means the owner's namespace.
type ObjectReference struct {
// ModelConfigReference identifies a model.
// +kubebuilder:validation:XValidation:rule="!has(self.group) || self.group == 'api.kagent.dev' || (has(self.kind) && self.kind != 'ModelConfig')",message="a group other than api.kagent.dev requires its own kind"
type ModelConfigReference struct {
// Group is the referenced API group. It defaults to api.kagent.dev.
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
// +optional
Group *string `json:"group,omitempty"`
Comment thread
codyhartsook marked this conversation as resolved.
Resolved
// Kind is the referenced kind. It defaults to ModelConfig.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$`
// +optional
Kind *string `json:"kind,omitempty"`
// Name is the referenced model's name.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
// +required
Name string `json:"name"`
// Namespace is the referenced model's namespace. Empty means the owner's namespace.
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
// +optional
Namespace string `json:"namespace,omitempty"`
}

// NamespacedName resolves the reference relative to its owner.
func (r ObjectReference) NamespacedName(ownerNamespace string) types.NamespacedName {
if r.Namespace != "" {
ownerNamespace = r.Namespace
}
return types.NamespacedName{Namespace: ownerNamespace, Name: r.Name}
// GroupKind returns the referenced kind, defaulting to ModelConfig.
func (r ModelConfigReference) GroupKind() schema.GroupKind {
return referenceGroupKind(r.Group, r.Kind, ModelConfigGroupKind)
}

// Target returns the referenced kind and key, defaulting to the owner's namespace.
func (r ModelConfigReference) Target(owner string) (schema.GroupKind, types.NamespacedName) {
return r.GroupKind(), types.NamespacedName{Namespace: cmp.Or(r.Namespace, owner), Name: r.Name}
}

// MCPServerReference identifies an MCP server.
// +kubebuilder:validation:XValidation:rule="!has(self.apiGroup)",message="use group instead of apiGroup"
// +kubebuilder:validation:XValidation:rule="!has(self.group) || self.group == 'api.kagent.dev' || (has(self.kind) && self.kind != 'RemoteMCPServer')",message="a group other than api.kagent.dev requires its own kind"
type MCPServerReference struct {
// v1.0.x Go clients, including the kagent CLI, send apiGroup as null. The gRPC
// API and strict field validation reject unknown fields, so the field stays.

// APIGroup is not supported.
//
// Deprecated: use Group.
// +optional
APIGroup *string `json:"apiGroup,omitempty"`
// Group is the referenced API group. It defaults to api.kagent.dev.
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
// +optional
Group *string `json:"group,omitempty"`
// v1.0.x required kind. The schema default stores it, so a v1.0.x CRD or Go client
// can still update objects written without it.

// Kind is the referenced kind. It defaults to RemoteMCPServer.
// +kubebuilder:default=RemoteMCPServer
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$`
// +optional
Kind *string `json:"kind,omitempty"`
// Name is the referenced server's name.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +required
Name string `json:"name"`
Comment thread
codyhartsook marked this conversation as resolved.
Resolved
// Namespace is the referenced server's namespace. Empty means the owner's namespace.
// +kubebuilder:validation:MaxLength=63
// +optional
Namespace string `json:"namespace,omitempty"`
Comment thread
codyhartsook marked this conversation as resolved.
Resolved
}

// GroupKind returns the referenced kind, defaulting to RemoteMCPServer.
func (r MCPServerReference) GroupKind() schema.GroupKind {
return referenceGroupKind(r.Group, r.Kind, RemoteMCPServerGroupKind)
}

// Target returns the referenced kind and key, defaulting to the owner's namespace.
func (r MCPServerReference) Target(owner string) (schema.GroupKind, types.NamespacedName) {
return r.GroupKind(), types.NamespacedName{Namespace: cmp.Or(r.Namespace, owner), Name: r.Name}
}

// referenceGroupKind fills an omitted group or kind from the field's default.
func referenceGroupKind(group, kind *string, defaultKind schema.GroupKind) schema.GroupKind {
return schema.GroupKind{Group: ptr.Deref(group, defaultKind.Group), Kind: ptr.Deref(kind, defaultKind.Kind)}
}

// ConfigMapKeyReference identifies a key in a same-namespace ConfigMap.
Expand Down
Loading
Loading